Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement multiple version in instance group manager #1499

Merged
merged 7 commits into from
Jun 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 198 additions & 76 deletions google/resource_compute_instance_group_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,50 @@ func resourceComputeInstanceGroupManager() *schema.Resource {

"instance_template": &schema.Schema{
Type: schema.TypeString,
Required: true,
Optional: true,
DiffSuppressFunc: compareSelfLinkRelativePaths,
},

"version": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this and instance_template within it are also going to need to be Computed. When I run tests that don't set a version, it seems to want to fill one in:

=== RUN   TestAccInstanceGroupManager_autoHealingPolicies
--- FAIL: TestAccInstanceGroupManager_autoHealingPolicies (245.15s)
    testing.go:513: Step 0 error: After applying this step, the plan was not empty:
        
        DIFF:
        
        UPDATE: google_compute_instance_group_manager.igm-basic
          version.#:                   "1" => "0"
          version.0.instance_template: "https://www.googleapis.com/compute/v1/projects/hc-terraform-testing/global/instanceTemplates/igm-test-bhvjqh6a61" => ""
        
        STATE:
        
        google_compute_http_health_check.zero:
          ID = igm-test-71h0sxbiwk
          provider = provider.google
          check_interval_sec = 1
          creation_timestamp = 2018-05-24T16:45:48.830-07:00
          description = 
          healthy_threshold = 2
          host = 
          name = igm-test-71h0sxbiwk
          port = 80
          project = hc-terraform-testing
          request_path = /
          self_link = https://www.googleapis.com/compute/v1/projects/hc-terraform-testing/global/httpHealthChecks/igm-test-71h0sxbiwk
          timeout_sec = 1
          unhealthy_threshold = 2
        google_compute_instance_group_manager.igm-basic:
          ID = igm-test-hu2dq1417r
          provider = provider.google
          auto_healing_policies.# = 1
          auto_healing_policies.0.health_check = https://www.googleapis.com/compute/beta/projects/hc-terraform-testing/global/httpHealthChecks/igm-test-71h0sxbiwk
          auto_healing_policies.0.initial_delay_sec = 10
          base_instance_name = igm-basic
          description = Terraform test instance group manager
          fingerprint = vF4q-P5d_aI=
          instance_group = https://www.googleapis.com/compute/v1/projects/hc-terraform-testing/zones/us-central1-c/instanceGroups/igm-test-hu2dq1417r
          instance_template = https://www.googleapis.com/compute/v1/projects/hc-terraform-testing/global/instanceTemplates/igm-test-bhvjqh6a61
          name = igm-test-hu2dq1417r
          named_port.# = 0
          project = hc-terraform-testing
          self_link = https://www.googleapis.com/compute/v1/projects/hc-terraform-testing/zones/us-central1-c/instanceGroupManagers/igm-test-hu2dq1417r
          target_pools.# = 1
          target_pools.1124437731 = https://www.googleapis.com/compute/beta/projects/hc-terraform-testing/regions/us-central1/targetPools/igm-test-u3zeiq2g3x
          target_size = 2
          update_strategy = RESTART
          version.# = 1
          version.0.instance_template = https://www.googleapis.com/compute/v1/projects/hc-terraform-testing/global/instanceTemplates/igm-test-bhvjqh6a61
          version.0.name = 
          version.0.target_size_fixed = 0
          version.0.target_size_percent = 0
          wait_for_instances = false
          zone = us-central1-c
        
          Dependencies:
            google_compute_http_health_check.zero
            google_compute_instance_template.igm-basic
            google_compute_target_pool.igm-basic
        google_compute_instance_template.igm-basic:
          ID = igm-test-bhvjqh6a61
          provider = provider.google
          can_ip_forward = false
          description = 
          disk.# = 1
          disk.0.auto_delete = true
          disk.0.boot = true
          disk.0.device_name = persistent-disk-0
          disk.0.disk_name = 
          disk.0.disk_size_gb = 0
          disk.0.disk_type = pd-standard
          disk.0.interface = SCSI
          disk.0.mode = READ_WRITE
          disk.0.source = 
          disk.0.source_image = debian-cloud/debian-8-jessie-v20160803
          disk.0.type = PERSISTENT
          instance_description = 
          machine_type = n1-standard-1
          metadata.% = 1
          metadata.foo = bar
          metadata_fingerprint = EEf91SmZULE=
          min_cpu_platform = 
          name = igm-test-bhvjqh6a61
          network_interface.# = 1
          network_interface.0.access_config.# = 0
          network_interface.0.address = 
          network_interface.0.alias_ip_range.# = 0
          network_interface.0.network = https://www.googleapis.com/compute/beta/projects/hc-terraform-testing/global/networks/default
          network_interface.0.network_ip = 
          network_interface.0.subnetwork = 
          network_interface.0.subnetwork_project = 
          project = hc-terraform-testing
          scheduling.# = 1
          scheduling.0.automatic_restart = true
          scheduling.0.on_host_maintenance = MIGRATE
          scheduling.0.preemptible = false
          self_link = https://www.googleapis.com/compute/beta/projects/hc-terraform-testing/global/instanceTemplates/igm-test-bhvjqh6a61
          service_account.# = 1
          service_account.0.email = default
          service_account.0.scopes.# = 3
          service_account.0.scopes.1632638332 = https://www.googleapis.com/auth/devstorage.read_only
          service_account.0.scopes.2428168921 = https://www.googleapis.com/auth/userinfo.email
          service_account.0.scopes.2862113455 = https://www.googleapis.com/auth/compute.readonly
          tags.# = 2
          tags.1996459178 = bar
          tags.2356372769 = foo
          tags_fingerprint = 
        google_compute_target_pool.igm-basic:
          ID = igm-test-u3zeiq2g3x
          provider = provider.google
          backup_pool = 
          description = Resource created for Terraform acceptance testing
          failover_ratio = 0
          health_checks.# = 0
          instances.# = 0
          name = igm-test-u3zeiq2g3x
          project = hc-terraform-testing
          region = us-central1
          self_link = https://www.googleapis.com/compute/v1/projects/hc-terraform-testing/regions/us-central1/targetPools/igm-test-u3zeiq2g3x
          session_affinity = CLIENT_IP_PROTO
FAIL

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made version optional and computed, but I cannot make instance_template Computed and Required, shouldn't we leave it Required ? (it is required within the version)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, that should be fine then. I'll run the test again and check it out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just ran tests and still hit the perpetual diff issue. Making version Optional & Computed is apparently not enough.

Here is the error:

UPDATE: google_compute_instance_group_manager.igm-basic
  version.#: "" => "<computed>"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to make tests pass by removing "Computed" flag, and now I see only one test failing (the one I wrote), because of the ImportStateVerify flag in my TestStep.

Do I need to add something to make version fields importable ?

--- FAIL: TestAccInstanceGroupManager_versions (137.02s)
        testing.go:513: Step 1 error: ImportStateVerify attributes not equivalent. Difference is shown below. Top is actual, bottom is expected.
                
                (map[string]string) {
                }
                
                
                (map[string]string) (len=9) {
                 (string) (len=9) "version.#": (string) (len=1) "2",
                 (string) (len=27) "version.0.instance_template": (string) (len=122) "https://www.googleapis.com/compute/beta/projects/myproject/global/instanceTemplates/igm-test-q1y1jv
92jt",
                 (string) (len=14) "version.0.name": (string) (len=7) "primary",
                 (string) (len=23) "version.0.target_size.#": (string) (len=1) "0",
                 (string) (len=27) "version.1.instance_template": (string) (len=122) "https://www.googleapis.com/compute/beta/projects/myproject/global/instanceTemplates/igm-test-c4hgbmva8w",
                 (string) (len=14) "version.1.name": (string) (len=6) "canary",
                 (string) (len=23) "version.1.target_size.#": (string) (len=1) "1",
                 (string) (len=29) "version.1.target_size.0.fixed": (string) (len=1) "1",
                 (string) (len=31) "version.1.target_size.0.percent": (string) (len=1) "0"
                }               

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some debugging on my end and was able to get it working- the d.Set call for versions was failing, which is why the import test failed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh thank you, I should have catch this one, this is all my bad! Sorry for that!

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,
Expand Down Expand Up @@ -138,6 +178,7 @@ func resourceComputeInstanceGroupManager() *schema.Resource {
},
},
},

"rolling_update_policy": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Expand Down Expand Up @@ -192,6 +233,7 @@ func resourceComputeInstanceGroupManager() *schema.Resource {
},
},
},

"wait_for_instances": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Expand Down Expand Up @@ -254,6 +296,7 @@ func resourceComputeInstanceGroupManagerCreate(d *schema.ResourceData, meta inte
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{})),
// Force send TargetSize to allow a value of 0.
ForceSendFields: []string{"TargetSize"},
}
Expand Down Expand Up @@ -290,6 +333,31 @@ func flattenNamedPortsBeta(namedPorts []*computeBeta.NamedPort) []map[string]int

}

func flattenVersions(versions []*computeBeta.InstanceGroupManagerVersion) []map[string]interface{} {
result := make([]map[string]interface{}, 0, len(versions))
for _, version := range versions {
versionMap := make(map[string]interface{})
versionMap["name"] = version.Name
versionMap["instance_template"] = ConvertSelfLinkToV1(version.InstanceTemplate)
versionMap["target_size"] = flattenFixedOrPercent(version.TargetSize)
result = append(result, versionMap)
}

return result
}

func flattenFixedOrPercent(fixedOrPercent *computeBeta.FixedOrPercent) []map[string]interface{} {
result := make(map[string]interface{})
if value := fixedOrPercent.Percent; value > 0 {
result["percent"] = value
} else if value := fixedOrPercent.Fixed; value > 0 {
result["fixed"] = fixedOrPercent.Fixed
} else {
return []map[string]interface{}{}
}
return []map[string]interface{}{result}
}

func getManager(d *schema.ResourceData, meta interface{}) (*computeBeta.InstanceGroupManager, error) {
config := meta.(*Config)

Expand Down Expand Up @@ -352,6 +420,9 @@ func resourceComputeInstanceGroupManagerRead(d *schema.ResourceData, meta interf

d.Set("base_instance_name", manager.BaseInstanceName)
d.Set("instance_template", ConvertSelfLinkToV1(manager.InstanceTemplate))
if err := d.Set("version", flattenVersions(manager.Versions)); err != nil {
return err
}
d.Set("name", manager.Name)
d.Set("zone", GetResourceNameFromSelfLink(manager.Zone))
d.Set("description", manager.Description)
Expand Down Expand Up @@ -385,6 +456,63 @@ 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 {
if updateStrategy == "RESTART" {
managedInstances, err := config.clientComputeBeta.InstanceGroupManagers.ListManagedInstances(project, zone, id).Do()
if err != nil {
return fmt.Errorf("Error getting 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.InstanceGroupManagersRecreateInstancesRequest{
Instances: instances,
}

op, err := config.clientComputeBeta.InstanceGroupManagers.RecreateInstances(project, zone, id, recreateInstances).Do()
if err != nil {
return fmt.Errorf("Error restarting instance group managers instances: %s", err)
}

// Wait for the operation to complete
err = computeSharedOperationWaitTime(config.clientCompute, op, project, managedInstanceCount*4, "Restarting InstanceGroupManagers 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.InstanceGroupManagers.Patch(project, zone, 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
}
}

return nil
}

func resourceComputeInstanceGroupManagerUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

Expand Down Expand Up @@ -430,81 +558,6 @@ func resourceComputeInstanceGroupManagerUpdate(d *schema.ResourceData, meta inte
d.SetPartial("target_pools")
}

// If instance_template changes then update
if d.HasChange("instance_template") {
// Build the parameter
setInstanceTemplate := &computeBeta.InstanceGroupManagersSetInstanceTemplateRequest{
InstanceTemplate: d.Get("instance_template").(string),
}

op, err := config.clientComputeBeta.InstanceGroupManagers.SetInstanceTemplate(
project, zone, d.Id(), setInstanceTemplate).Do()

if err != nil {
return fmt.Errorf("Error updating InstanceGroupManager: %s", err)
}

// Wait for the operation to complete
err = computeSharedOperationWait(config.clientCompute, op, project, "Updating InstanceGroupManager")
if err != nil {
return err
}

if d.Get("update_strategy").(string) == "RESTART" {
managedInstances, err := config.clientComputeBeta.InstanceGroupManagers.ListManagedInstances(
project, zone, d.Id()).Do()
if err != nil {
return fmt.Errorf("Error getting 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.InstanceGroupManagersRecreateInstancesRequest{
Instances: instances,
}

op, err = config.clientComputeBeta.InstanceGroupManagers.RecreateInstances(
project, zone, d.Id(), recreateInstances).Do()
if err != nil {
return fmt.Errorf("Error restarting instance group managers instances: %s", err)
}

// Wait for the operation to complete
err = computeSharedOperationWaitTime(config.clientCompute, op, project, managedInstanceCount*4, "Restarting InstanceGroupManagers instances")
if err != nil {
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{})),
}

op, err = config.clientComputeBeta.InstanceGroupManagers.Patch(
project, zone, 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
}
}

d.SetPartial("instance_template")
}

// If named_port changes then update:
if d.HasChange("named_port") {

Expand Down Expand Up @@ -572,6 +625,44 @@ func resourceComputeInstanceGroupManagerUpdate(d *schema.ResourceData, meta inte
d.SetPartial("auto_healing_policies")
}

// If instance_template changes then update
if d.HasChange("instance_template") {
// Build the parameter
setInstanceTemplate := &computeBeta.InstanceGroupManagersSetInstanceTemplateRequest{
InstanceTemplate: d.Get("instance_template").(string),
}

op, err := config.clientComputeBeta.InstanceGroupManagers.SetInstanceTemplate(project, zone, d.Id(), setInstanceTemplate).Do()

if err != nil {
return fmt.Errorf("Error updating InstanceGroupManager: %s", err)
}

// Wait for the operation to complete
err = computeSharedOperationWait(config.clientCompute, op, project, "Updating InstanceGroupManager")
if err != nil {
return err
}

updateStrategy := d.Get("update_strategy").(string)
rollingUpdatePolicy := expandUpdatePolicy(d.Get("rolling_update_policy").([]interface{}))
err = performUpdate(config, d.Id(), updateStrategy, rollingUpdatePolicy, nil, project, zone)
d.SetPartial("instance_template")
}

// 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 = performUpdate(config, d.Id(), updateStrategy, rollingUpdatePolicy, versions, project, zone)
if err != nil {
return err
}

d.SetPartial("version")
}

d.Partial(false)

return resourceComputeInstanceGroupManagerRead(d, meta)
Expand Down Expand Up @@ -647,6 +738,37 @@ func expandAutoHealingPolicies(configured []interface{}) []*computeBeta.Instance
return autoHealingPolicies
}

func expandVersions(configured []interface{}) []*computeBeta.InstanceGroupManagerVersion {
versions := make([]*computeBeta.InstanceGroupManagerVersion, 0, len(configured))
for _, raw := range configured {
data := raw.(map[string]interface{})

version := computeBeta.InstanceGroupManagerVersion{
Name: data["name"].(string),
InstanceTemplate: data["instance_template"].(string),
TargetSize: expandFixedOrPercent(data["target_size"].([]interface{})),
}

versions = append(versions, &version)
}
return versions
}

func expandFixedOrPercent(configured []interface{}) *computeBeta.FixedOrPercent {
fixedOrPercent := &computeBeta.FixedOrPercent{}

for _, raw := range configured {
data := raw.(map[string]interface{})
if percent := data["percent"]; percent.(int) > 0 {
fixedOrPercent.Percent = int64(percent.(int))
} else {
fixedOrPercent.Fixed = int64(data["fixed"].(int))
fixedOrPercent.ForceSendFields = []string{"Fixed"}
}
}
return fixedOrPercent
}

func expandUpdatePolicy(configured []interface{}) *computeBeta.InstanceGroupManagerUpdatePolicy {
updatePolicy := &computeBeta.InstanceGroupManagerUpdatePolicy{}

Expand Down
Loading