diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go index fc47756c794b..11258b36f9a8 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group.go @@ -109,6 +109,13 @@ func resourceAwsAutoscalingGroup() *schema.Resource { Set: schema.HashString, }, + "suspended_processes": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "vpc_zone_identifier": &schema.Schema{ Type: schema.TypeSet, Optional: true, @@ -232,10 +239,15 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) return fmt.Errorf("Error creating Autoscaling Group: %s", err) } + if err := updateSuspendedProcesses(d, meta); err != nil { + return err + } + d.SetId(d.Get("name").(string)) + log.Printf("[INFO] AutoScaling Group ID: %s", d.Id()) - if err := waitForASGCapacity(d, meta, capacitySatifiedCreate); err != nil { + if err := waitForASGCapacity(d, meta); err != nil { return err } @@ -273,6 +285,7 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e d.Set("max_size", g.MaxSize) d.Set("placement_group", g.PlacementGroup) d.Set("name", g.AutoScalingGroupName) + d.Set("suspended_processes", flattenSuspendedProcesses(g.SuspendedProcesses)) d.Set("tag", g.Tags) d.Set("vpc_zone_identifier", strings.Split(*g.VPCZoneIdentifier, ",")) @@ -298,7 +311,6 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).autoscalingconn - shouldWaitForCapacity := false opts := autoscaling.UpdateAutoScalingGroupInput{ AutoScalingGroupName: aws.String(d.Id()), @@ -310,7 +322,6 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) if d.HasChange("desired_capacity") { opts.DesiredCapacity = aws.Int64(int64(d.Get("desired_capacity").(int))) - shouldWaitForCapacity = true } if d.HasChange("launch_configuration") { @@ -319,7 +330,6 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) if d.HasChange("min_size") { opts.MinSize = aws.Int64(int64(d.Get("min_size").(int))) - shouldWaitForCapacity = true } if d.HasChange("max_size") { @@ -360,12 +370,16 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) } } + if err := updateSuspendedProcesses(d, meta); err != nil { + return err + } + if err := setAutoscalingTags(conn, d); err != nil { return err - } else { - d.SetPartial("tag") } + d.SetPartial("tag") + log.Printf("[DEBUG] AutoScaling Group update configuration: %#v", opts) _, err := conn.UpdateAutoScalingGroup(&opts) if err != nil { @@ -409,8 +423,8 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) } } - if shouldWaitForCapacity { - waitForASGCapacity(d, meta, capacitySatifiedUpdate) + if err := waitForASGCapacity(d, meta); err != nil { + return err } if d.HasChange("enabled_metrics") { @@ -643,3 +657,46 @@ func expandVpcZoneIdentifiers(list []interface{}) *string { } return aws.String(strings.Join(strs, ",")) } + +func flattenSuspendedProcesses(procs []*autoscaling.SuspendedProcess) []string { + names := make([]string, len(procs)) + for i, p := range procs { + names[i] = *p.ProcessName + } + return names +} + +func updateSuspendedProcesses(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + + if d.HasChange("suspended_processes") { + old, new := d.GetChange("suspended_processes") + oldSet := old.(*schema.Set) + newSet := new.(*schema.Set) + + added := newSet.Difference(oldSet).List() + if len(added) > 0 { + _, err := conn.SuspendProcesses(&autoscaling.ScalingProcessQuery{ + AutoScalingGroupName: aws.String(d.Get("name").(string)), + ScalingProcesses: expandStringList(added), + }) + if err != nil { + return fmt.Errorf("Failure suspending scaling processes: %s", err) + } + } + + removed := oldSet.Difference(newSet).List() + if len(removed) > 0 { + _, err := conn.ResumeProcesses(&autoscaling.ScalingProcessQuery{ + AutoScalingGroupName: aws.String(d.Get("name").(string)), + ScalingProcesses: expandStringList(removed), + }) + if err != nil { + return fmt.Errorf("Failure resuming scaling processes: %s", err) + } + } + d.SetPartial("suspended_processes") + } + + return nil +} diff --git a/builtin/providers/aws/resource_aws_autoscaling_group_test.go b/builtin/providers/aws/resource_aws_autoscaling_group_test.go index ed4d559badb5..8a879760736c 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_test.go @@ -136,6 +136,41 @@ func TestAccAWSAutoScalingGroup_terminationPolicies(t *testing.T) { }) } +func TestAccAWSAutoScalingGroup_suspendedProcceses(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSAutoScalingGroupConfig_suspendedProcessesNone, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "aws_autoscaling_group.bar", "suspended_processes.#", "0"), + ), + }, + resource.TestStep{ + Config: testAccAWSAutoScalingGroupConfig_suspendedProcessesUpdate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "aws_autoscaling_group.bar", "suspended_processes.#", "2"), + resource.TestCheckResourceAttr( + "aws_autoscaling_group.bar", "suspended_processes.997436260", "HealthCheck"), + resource.TestCheckResourceAttr( + "aws_autoscaling_group.bar", "suspended_processes.2282213524", "ScheduledActions"), + ), + }, + resource.TestStep{ + Config: testAccAWSAutoScalingGroupConfig_suspendedProcessesNone, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "aws_autoscaling_group.bar", "suspended_processes.#", "0"), + ), + }, + }, + }) +} + func TestAccAWSAutoScalingGroup_tags(t *testing.T) { var group autoscaling.Group @@ -539,6 +574,41 @@ resource "aws_autoscaling_group" "bar" { } ` +const testAccAWSAutoScalingGroupConfig_suspendedProcessesNone = ` +resource "aws_launch_configuration" "foobar" { + image_id = "ami-21f78e11" + instance_type = "t1.micro" +} + +resource "aws_autoscaling_group" "bar" { + availability_zones = ["us-west-2a"] + desired_capacity = 0 + max_size = 0 + min_size = 0 + launch_configuration = "${aws_launch_configuration.foobar.name}" +} +` + +const testAccAWSAutoScalingGroupConfig_suspendedProcessesUpdate = ` +resource "aws_launch_configuration" "foobar" { + image_id = "ami-21f78e11" + instance_type = "t1.micro" +} + +resource "aws_autoscaling_group" "bar" { + availability_zones = ["us-west-2a"] + desired_capacity = 0 + max_size = 0 + min_size = 0 + launch_configuration = "${aws_launch_configuration.foobar.name}" + + suspended_processes = [ + "HealthCheck", + "ScheduledActions", + ] +} +` + const testAccAWSAutoScalingGroupConfig = ` resource "aws_launch_configuration" "foobar" { image_id = "ami-21f78e11" diff --git a/builtin/providers/aws/resource_aws_autoscaling_group_waiting.go b/builtin/providers/aws/resource_aws_autoscaling_group_waiting.go index bfafa9b3364f..29f73b6b0f20 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_waiting.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_waiting.go @@ -16,10 +16,7 @@ import ( // the capacitySatisfiedFunc returns true. // // See "Waiting for Capacity" in docs for more discussion of the feature. -func waitForASGCapacity( - d *schema.ResourceData, - meta interface{}, - satisfiedFunc capacitySatisfiedFunc) error { +func waitForASGCapacity(d *schema.ResourceData, meta interface{}) error { wait, err := time.ParseDuration(d.Get("wait_for_capacity_timeout").(string)) if err != nil { return err @@ -77,7 +74,7 @@ func waitForASGCapacity( } } - satisfied, reason := satisfiedFunc(d, haveASG, haveELB) + satisfied, reason := checkCapacitySatisfied(d, haveASG, haveELB) log.Printf("[DEBUG] %q Capacity: %d ASG, %d ELB, satisfied: %t, reason: %q", d.Id(), haveASG, haveELB, satisfied, reason) @@ -91,42 +88,55 @@ func waitForASGCapacity( }) } -type capacitySatisfiedFunc func(*schema.ResourceData, int, int) (bool, string) +// checkCapacitySatisfied determines if required ASG and ELB targets are met. +func checkCapacitySatisfied(d *schema.ResourceData, haveASG, haveELB int) (bool, string) { + isLaunchSuspended := false + isTerminateSuspended := false + if procs, ok := d.GetOk("suspended_processes"); ok { + for _, p := range procs.(*schema.Set).List() { + switch p.(string) { + case "Launch": + isLaunchSuspended = true + case "Terminate": + isTerminateSuspended = true + } + } + } -// capacitySatifiedCreate treats all targets as minimums -func capacitySatifiedCreate(d *schema.ResourceData, haveASG, haveELB int) (bool, string) { - minASG := d.Get("min_size").(int) - if wantASG := d.Get("desired_capacity").(int); wantASG > 0 { - minASG = wantASG + if desiredASG, ok := d.GetOk("desired_capacity"); ok && desiredASG.(int) > haveASG && !isLaunchSuspended { + return false, fmt.Sprintf( + "Need exactly %d healthy instances in ASG, have %d", desiredASG.(int), haveASG) } - if haveASG < minASG { + + if desiredASG, ok := d.GetOk("desired_capacity"); ok && desiredASG.(int) < haveASG && !isTerminateSuspended { return false, fmt.Sprintf( - "Need at least %d healthy instances in ASG, have %d", minASG, haveASG) + "Need exactly %d healthy instances in ASG, have %d", desiredASG.(int), haveASG) } - minELB := d.Get("min_elb_capacity").(int) - if wantELB := d.Get("wait_for_elb_capacity").(int); wantELB > 0 { - minELB = wantELB + + if minASG, ok := d.GetOk("min_size"); ok && minASG.(int) > haveASG && !isLaunchSuspended { + return false, fmt.Sprintf( + "Need at least %d healthy instances in ASG, have %d", minASG.(int), haveASG) } - if haveELB < minELB { + + if maxASG, ok := d.GetOk("max_size"); ok && maxASG.(int) < haveASG && !isTerminateSuspended { return false, fmt.Sprintf( - "Need at least %d healthy instances in ELB, have %d", minELB, haveELB) + "Need at most %d healthy instances in ASG, have %d", maxASG.(int), haveASG) } - return true, "" -} -// capacitySatifiedUpdate only cares about specific targets -func capacitySatifiedUpdate(d *schema.ResourceData, haveASG, haveELB int) (bool, string) { - if wantASG := d.Get("desired_capacity").(int); wantASG > 0 { - if haveASG != wantASG { - return false, fmt.Sprintf( - "Need exactly %d healthy instances in ASG, have %d", wantASG, haveASG) - } + if desiredELB, ok := d.GetOk("wait_for_elb_capacity"); ok && desiredELB.(int) > haveELB && !isLaunchSuspended { + return false, fmt.Sprintf( + "Need exactly %d healthy instances in ELB, have %d", desiredELB.(int), haveELB) } - if wantELB := d.Get("wait_for_elb_capacity").(int); wantELB > 0 { - if haveELB != wantELB { - return false, fmt.Sprintf( - "Need exactly %d healthy instances in ELB, have %d", wantELB, haveELB) - } + + if desiredELB, ok := d.GetOk("wait_for_elb_capacity"); ok && desiredELB.(int) < haveELB && !isTerminateSuspended { + return false, fmt.Sprintf( + "Need exactly %d healthy instances in ELB, have %d", desiredELB.(int), haveELB) } + + if minELB, ok := d.GetOk("min_elb_capacity"); ok && minELB.(int) > haveELB && !isLaunchSuspended { + return false, fmt.Sprintf( + "Need at least %d healthy instances in ELB, have %d", minELB.(int), haveELB) + } + return true, "" } diff --git a/builtin/providers/aws/resource_aws_autoscaling_group_waiting_test.go b/builtin/providers/aws/resource_aws_autoscaling_group_waiting_test.go index d7c3895c0e17..2098c11a5c57 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_waiting_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_waiting_test.go @@ -2,7 +2,9 @@ package aws import "testing" -func TestCapacitySatisfiedCreate(t *testing.T) { +import "github.com/hashicorp/terraform/helper/schema" + +func TestCapacitySatisfied(t *testing.T) { cases := map[string]struct { Data map[string]interface{} HaveASG int @@ -18,6 +20,14 @@ func TestCapacitySatisfiedCreate(t *testing.T) { ExpectSatisfied: false, ExpectReason: "Need at least 5 healthy instances in ASG, have 2", }, + "min_size, have less, launch suspended": { + Data: map[string]interface{}{ + "min_size": 5, + "suspended_processes": schema.NewSet(schema.HashString, []interface{}{"Launch"}), + }, + HaveASG: 2, + ExpectSatisfied: true, + }, "min_size, got it": { Data: map[string]interface{}{ "min_size": 5, @@ -38,7 +48,24 @@ func TestCapacitySatisfiedCreate(t *testing.T) { }, HaveASG: 2, ExpectSatisfied: false, - ExpectReason: "Need at least 5 healthy instances in ASG, have 2", + ExpectReason: "Need exactly 5 healthy instances in ASG, have 2", + }, + "desired_capacity, have less, launch suspended": { + Data: map[string]interface{}{ + "desired_capacity": 5, + "suspended_processes": schema.NewSet(schema.HashString, []interface{}{"Launch"}), + }, + HaveASG: 2, + ExpectSatisfied: true, + }, + "desired_capacity, have less, terminate suspended": { + Data: map[string]interface{}{ + "desired_capacity": 5, + "suspended_processes": schema.NewSet(schema.HashString, []interface{}{"Terminate"}), + }, + HaveASG: 2, + ExpectSatisfied: false, + ExpectReason: "Need exactly 5 healthy instances in ASG, have 2", }, "desired_capacity overrides min_size": { Data: map[string]interface{}{ @@ -47,7 +74,7 @@ func TestCapacitySatisfiedCreate(t *testing.T) { }, HaveASG: 2, ExpectSatisfied: false, - ExpectReason: "Need at least 5 healthy instances in ASG, have 2", + ExpectReason: "Need exactly 5 healthy instances in ASG, have 2", }, "desired_capacity, got it": { Data: map[string]interface{}{ @@ -56,133 +83,97 @@ func TestCapacitySatisfiedCreate(t *testing.T) { HaveASG: 5, ExpectSatisfied: true, }, - "desired_capacity, have more": { + "max_size, have less": { Data: map[string]interface{}{ - "desired_capacity": 5, + "max_size": 5, }, - HaveASG: 10, + HaveASG: 2, ExpectSatisfied: true, }, - - "min_elb_capacity, have less": { + "max_size, have more": { Data: map[string]interface{}{ - "min_elb_capacity": 5, + "max_size": 5, }, - HaveELB: 2, + HaveASG: 10, ExpectSatisfied: false, - ExpectReason: "Need at least 5 healthy instances in ELB, have 2", + ExpectReason: "Need at most 5 healthy instances in ASG, have 10", }, - "min_elb_capacity, got it": { + "max_size, have more, terminate suspended": { Data: map[string]interface{}{ - "min_elb_capacity": 5, + "max_size": 5, + "suspended_processes": schema.NewSet(schema.HashString, []interface{}{"Terminate"}), }, - HaveELB: 5, + HaveASG: 10, ExpectSatisfied: true, }, - "min_elb_capacity, have more": { + "max_size, got it": { Data: map[string]interface{}{ - "min_elb_capacity": 5, + "max_size": 5, }, - HaveELB: 10, + HaveASG: 5, ExpectSatisfied: true, }, - "wait_for_elb_capacity, have less": { + "min_size, max_size, between": { Data: map[string]interface{}{ - "wait_for_elb_capacity": 5, + "min_size": 1, + "max_size": 5, + }, + HaveASG: 2, + ExpectSatisfied: true, + }, + "min_elb_capacity, have less": { + Data: map[string]interface{}{ + "min_elb_capacity": 5, }, HaveELB: 2, ExpectSatisfied: false, ExpectReason: "Need at least 5 healthy instances in ELB, have 2", }, - "wait_for_elb_capacity, got it": { + "min_elb_capacity, have less, launch suspended": { Data: map[string]interface{}{ - "wait_for_elb_capacity": 5, + "min_elb_capacity": 5, + "suspended_processes": schema.NewSet(schema.HashString, []interface{}{"Launch"}), }, - HaveELB: 5, + HaveELB: 2, ExpectSatisfied: true, }, - "wait_for_elb_capacity, have more": { + "min_elb_capacity, got it": { Data: map[string]interface{}{ - "wait_for_elb_capacity": 5, + "min_elb_capacity": 5, }, - HaveELB: 10, + HaveELB: 5, ExpectSatisfied: true, }, - "wait_for_elb_capacity overrides min_elb_capacity": { + "min_elb_capacity, have more": { Data: map[string]interface{}{ - "min_elb_capacity": 2, - "wait_for_elb_capacity": 5, + "min_elb_capacity": 5, }, - HaveELB: 2, - ExpectSatisfied: false, - ExpectReason: "Need at least 5 healthy instances in ELB, have 2", - }, - } - - r := resourceAwsAutoscalingGroup() - for tn, tc := range cases { - d := r.TestResourceData() - for k, v := range tc.Data { - if err := d.Set(k, v); err != nil { - t.Fatalf("err: %s", err) - } - } - gotSatisfied, gotReason := capacitySatifiedCreate(d, tc.HaveASG, tc.HaveELB) - - if gotSatisfied != tc.ExpectSatisfied { - t.Fatalf("%s: expected satisfied: %t, got: %t (reason: %s)", - tn, tc.ExpectSatisfied, gotSatisfied, gotReason) - } - - if gotReason != tc.ExpectReason { - t.Fatalf("%s: expected reason: %s, got: %s", - tn, tc.ExpectReason, gotReason) - } - } -} - -func TestCapacitySatisfiedUpdate(t *testing.T) { - cases := map[string]struct { - Data map[string]interface{} - HaveASG int - HaveELB int - ExpectSatisfied bool - ExpectReason string - }{ - "default is satisfied": { - Data: map[string]interface{}{}, + HaveELB: 10, ExpectSatisfied: true, }, - "desired_capacity, have less": { + "wait_for_elb_capacity, have less": { Data: map[string]interface{}{ - "desired_capacity": 5, + "wait_for_elb_capacity": 5, }, - HaveASG: 2, + HaveELB: 2, ExpectSatisfied: false, - ExpectReason: "Need exactly 5 healthy instances in ASG, have 2", + ExpectReason: "Need exactly 5 healthy instances in ELB, have 2", }, - "desired_capacity, got it": { + "wait_for_elb_capacity, have less, launch suspended": { Data: map[string]interface{}{ - "desired_capacity": 5, + "wait_for_elb_capacity": 5, + "suspended_processes": schema.NewSet(schema.HashString, []interface{}{"Launch"}), }, - HaveASG: 5, + HaveELB: 2, ExpectSatisfied: true, }, - "desired_capacity, have more": { - Data: map[string]interface{}{ - "desired_capacity": 5, - }, - HaveASG: 10, - ExpectSatisfied: false, - ExpectReason: "Need exactly 5 healthy instances in ASG, have 10", - }, - "wait_for_elb_capacity, have less": { + "wait_for_elb_capacity, have more, terminate suspended": { Data: map[string]interface{}{ "wait_for_elb_capacity": 5, + "suspended_processes": schema.NewSet(schema.HashString, []interface{}{"Terminate"}), }, - HaveELB: 2, - ExpectSatisfied: false, - ExpectReason: "Need exactly 5 healthy instances in ELB, have 2", + HaveELB: 10, + ExpectSatisfied: true, }, "wait_for_elb_capacity, got it": { Data: map[string]interface{}{ @@ -191,13 +182,14 @@ func TestCapacitySatisfiedUpdate(t *testing.T) { HaveELB: 5, ExpectSatisfied: true, }, - "wait_for_elb_capacity, have more": { + "wait_for_elb_capacity overrides min_elb_capacity": { Data: map[string]interface{}{ + "min_elb_capacity": 2, "wait_for_elb_capacity": 5, }, - HaveELB: 10, + HaveELB: 2, ExpectSatisfied: false, - ExpectReason: "Need exactly 5 healthy instances in ELB, have 10", + ExpectReason: "Need exactly 5 healthy instances in ELB, have 2", }, } @@ -209,7 +201,7 @@ func TestCapacitySatisfiedUpdate(t *testing.T) { t.Fatalf("err: %s", err) } } - gotSatisfied, gotReason := capacitySatifiedUpdate(d, tc.HaveASG, tc.HaveELB) + gotSatisfied, gotReason := checkCapacitySatisfied(d, tc.HaveASG, tc.HaveELB) if gotSatisfied != tc.ExpectSatisfied { t.Fatalf("%s: expected satisfied: %t, got: %t (reason: %s)", diff --git a/website/source/docs/providers/aws/r/autoscaling_group.html.markdown b/website/source/docs/providers/aws/r/autoscaling_group.html.markdown index 233fb6cfe475..71a1d047033e 100644 --- a/website/source/docs/providers/aws/r/autoscaling_group.html.markdown +++ b/website/source/docs/providers/aws/r/autoscaling_group.html.markdown @@ -66,6 +66,8 @@ The following arguments are supported: behavior and potentially leaves resources dangling. * `load_balancers` (Optional) A list of load balancer names to add to the autoscaling group names. +* `suspended_processes` (Optional) A list of auto scaling processes to suspend. +(See [Suspending and Resuming Processes](http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/US_SuspendResume.html).) * `vpc_zone_identifier` (Optional) A list of subnet IDs to launch resources in. * `termination_policies` (Optional) A list of policies to decide how the instances in the auto scale group should be terminated. * `tag` (Optional) A list of tag blocks. Tags documented below.