diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go index a10c96cb4..375a156fd 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group.go @@ -181,16 +181,93 @@ func resourceAwsAutoscalingGroup() *schema.Resource { Computed: true, }, + "initial_lifecycle_hook": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "default_result": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "heartbeat_timeout": { + Type: schema.TypeInt, + Optional: true, + }, + "lifecycle_transition": { + Type: schema.TypeString, + Required: true, + }, + "notification_metadata": { + Type: schema.TypeString, + Optional: true, + }, + "notification_target_arn": { + Type: schema.TypeString, + Optional: true, + }, + "role_arn": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "tag": autoscalingTagsSchema(), }, } } +func generatePutLifecycleHookInputs(asgName string, cfgs []interface{}) []autoscaling.PutLifecycleHookInput { + res := make([]autoscaling.PutLifecycleHookInput, 0, len(cfgs)) + + for _, raw := range cfgs { + cfg := raw.(map[string]interface{}) + + input := autoscaling.PutLifecycleHookInput{ + AutoScalingGroupName: &asgName, + LifecycleHookName: aws.String(cfg["name"].(string)), + } + + if v, ok := cfg["default_result"]; ok && v.(string) != "" { + input.DefaultResult = aws.String(v.(string)) + } + + if v, ok := cfg["heartbeat_timeout"]; ok && v.(int) > 0 { + input.HeartbeatTimeout = aws.Int64(int64(v.(int))) + } + + if v, ok := cfg["lifecycle_transition"]; ok && v.(string) != "" { + input.LifecycleTransition = aws.String(v.(string)) + } + + if v, ok := cfg["notification_metadata"]; ok && v.(string) != "" { + input.NotificationMetadata = aws.String(v.(string)) + } + + if v, ok := cfg["notification_target_arn"]; ok && v.(string) != "" { + input.NotificationTargetARN = aws.String(v.(string)) + } + + if v, ok := cfg["role_arn"]; ok && v.(string) != "" { + input.RoleARN = aws.String(v.(string)) + } + + res = append(res, input) + } + + return res +} + func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).autoscalingconn - var autoScalingGroupOpts autoscaling.CreateAutoScalingGroupInput - var asgName string if v, ok := d.GetOk("name"); ok { asgName = v.(string) @@ -199,69 +276,106 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) d.Set("name", asgName) } - autoScalingGroupOpts.AutoScalingGroupName = aws.String(asgName) - autoScalingGroupOpts.LaunchConfigurationName = aws.String(d.Get("launch_configuration").(string)) - autoScalingGroupOpts.MinSize = aws.Int64(int64(d.Get("min_size").(int))) - autoScalingGroupOpts.MaxSize = aws.Int64(int64(d.Get("max_size").(int))) - autoScalingGroupOpts.NewInstancesProtectedFromScaleIn = aws.Bool(d.Get("protect_from_scale_in").(bool)) + createOpts := autoscaling.CreateAutoScalingGroupInput{ + AutoScalingGroupName: aws.String(asgName), + LaunchConfigurationName: aws.String(d.Get("launch_configuration").(string)), + NewInstancesProtectedFromScaleIn: aws.Bool(d.Get("protect_from_scale_in").(bool)), + } + updateOpts := autoscaling.UpdateAutoScalingGroupInput{ + AutoScalingGroupName: aws.String(asgName), + } + + initialLifecycleHooks := d.Get("initial_lifecycle_hook").(*schema.Set).List() + twoPhases := len(initialLifecycleHooks) > 0 + + minSize := aws.Int64(int64(d.Get("min_size").(int))) + maxSize := aws.Int64(int64(d.Get("max_size").(int))) + + if twoPhases { + createOpts.MinSize = aws.Int64(int64(0)) + createOpts.MaxSize = aws.Int64(int64(0)) + + updateOpts.MinSize = minSize + updateOpts.MaxSize = maxSize + + if v, ok := d.GetOk("desired_capacity"); ok { + updateOpts.DesiredCapacity = aws.Int64(int64(v.(int))) + } + } else { + createOpts.MinSize = minSize + createOpts.MaxSize = maxSize + + if v, ok := d.GetOk("desired_capacity"); ok { + createOpts.DesiredCapacity = aws.Int64(int64(v.(int))) + } + } // Availability Zones are optional if VPC Zone Identifer(s) are specified if v, ok := d.GetOk("availability_zones"); ok && v.(*schema.Set).Len() > 0 { - autoScalingGroupOpts.AvailabilityZones = expandStringList(v.(*schema.Set).List()) + createOpts.AvailabilityZones = expandStringList(v.(*schema.Set).List()) } if v, ok := d.GetOk("tag"); ok { - autoScalingGroupOpts.Tags = autoscalingTagsFromMap( + createOpts.Tags = autoscalingTagsFromMap( setToMapByKey(v.(*schema.Set), "key"), d.Get("name").(string)) } if v, ok := d.GetOk("default_cooldown"); ok { - autoScalingGroupOpts.DefaultCooldown = aws.Int64(int64(v.(int))) + createOpts.DefaultCooldown = aws.Int64(int64(v.(int))) } if v, ok := d.GetOk("health_check_type"); ok && v.(string) != "" { - autoScalingGroupOpts.HealthCheckType = aws.String(v.(string)) - } - - if v, ok := d.GetOk("desired_capacity"); ok { - autoScalingGroupOpts.DesiredCapacity = aws.Int64(int64(v.(int))) + createOpts.HealthCheckType = aws.String(v.(string)) } if v, ok := d.GetOk("health_check_grace_period"); ok { - autoScalingGroupOpts.HealthCheckGracePeriod = aws.Int64(int64(v.(int))) + createOpts.HealthCheckGracePeriod = aws.Int64(int64(v.(int))) } if v, ok := d.GetOk("placement_group"); ok { - autoScalingGroupOpts.PlacementGroup = aws.String(v.(string)) + createOpts.PlacementGroup = aws.String(v.(string)) } if v, ok := d.GetOk("load_balancers"); ok && v.(*schema.Set).Len() > 0 { - autoScalingGroupOpts.LoadBalancerNames = expandStringList( + createOpts.LoadBalancerNames = expandStringList( v.(*schema.Set).List()) } if v, ok := d.GetOk("vpc_zone_identifier"); ok && v.(*schema.Set).Len() > 0 { - autoScalingGroupOpts.VPCZoneIdentifier = expandVpcZoneIdentifiers(v.(*schema.Set).List()) + createOpts.VPCZoneIdentifier = expandVpcZoneIdentifiers(v.(*schema.Set).List()) } if v, ok := d.GetOk("termination_policies"); ok && len(v.([]interface{})) > 0 { - autoScalingGroupOpts.TerminationPolicies = expandStringList(v.([]interface{})) + createOpts.TerminationPolicies = expandStringList(v.([]interface{})) } if v, ok := d.GetOk("target_group_arns"); ok && len(v.(*schema.Set).List()) > 0 { - autoScalingGroupOpts.TargetGroupARNs = expandStringList(v.(*schema.Set).List()) + createOpts.TargetGroupARNs = expandStringList(v.(*schema.Set).List()) } - log.Printf("[DEBUG] AutoScaling Group create configuration: %#v", autoScalingGroupOpts) - _, err := conn.CreateAutoScalingGroup(&autoScalingGroupOpts) + log.Printf("[DEBUG] AutoScaling Group create configuration: %#v", createOpts) + _, err := conn.CreateAutoScalingGroup(&createOpts) if err != nil { - return fmt.Errorf("Error creating Autoscaling Group: %s", err) + return fmt.Errorf("Error creating AutoScaling Group: %s", 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 twoPhases { + for _, hook := range generatePutLifecycleHookInputs(asgName, initialLifecycleHooks) { + if err = resourceAwsAutoscalingLifecycleHookPutOp(conn, &hook); err != nil { + return fmt.Errorf("Error creating initial lifecycle hooks: %s", err) + } + } + + _, err = conn.UpdateAutoScalingGroup(&updateOpts) + if err != nil { + return fmt.Errorf("Error setting AutoScaling Group initial capacity: %s", err) + } + } + + if err := waitForASGCapacity(d, meta, capacitySatisfiedCreate); err != nil { return err } @@ -479,7 +593,7 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) } if shouldWaitForCapacity { - waitForASGCapacity(d, meta, capacitySatifiedUpdate) + waitForASGCapacity(d, meta, capacitySatisfiedUpdate) } if d.HasChange("enabled_metrics") { diff --git a/builtin/providers/aws/resource_aws_autoscaling_group_test.go b/builtin/providers/aws/resource_aws_autoscaling_group_test.go index a5fc59152..4b9f99e03 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_test.go @@ -399,6 +399,37 @@ func TestAccAWSAutoScalingGroup_ALB_TargetGroups(t *testing.T) { }) } +func TestAccAWSAutoScalingGroup_initialLifecycleHook(t *testing.T) { + var group autoscaling.Group + + randName := fmt.Sprintf("terraform-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_autoscaling_group.bar", + IDRefreshIgnore: []string{"force_delete", "metrics_granularity", "wait_for_capacity_timeout"}, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSAutoScalingGroupWithHookConfig(randName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group), + testAccCheckAWSAutoScalingGroupHealthyCapacity(&group, 2), + resource.TestCheckResourceAttr( + "aws_autoscaling_group.bar", "initial_lifecycle_hook.#", "1"), + resource.TestCheckResourceAttr( + "aws_autoscaling_group.bar", "initial_lifecycle_hook.391359060.default_result", "CONTINUE"), + resource.TestCheckResourceAttr( + "aws_autoscaling_group.bar", "initial_lifecycle_hook.391359060.name", "launching"), + testAccCheckAWSAutoScalingGroupInitialLifecycleHookExists( + "aws_autoscaling_group.bar", "initial_lifecycle_hook.391359060.name"), + ), + }, + }, + }) +} + func testAccCheckAWSAutoScalingGroupDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).autoscalingconn @@ -529,6 +560,26 @@ func testAccCheckAWSAutoScalingGroupExists(n string, group *autoscaling.Group) r } } +func testAccCheckAWSAutoScalingGroupInitialLifecycleHookExists(asg, hookAttr string) resource.TestCheckFunc { + return func(s *terraform.State) error { + asgResource, ok := s.RootModule().Resources[asg] + if !ok { + return fmt.Errorf("Not found: %s", asg) + } + + if asgResource.Primary.ID == "" { + return fmt.Errorf("No AutoScaling Group ID is set") + } + + hookName := asgResource.Primary.Attributes[hookAttr] + if hookName == "" { + return fmt.Errorf("ASG %s has no hook name %s", asg, hookAttr) + } + + return checkLifecycleHookExistsByName(asgResource.Primary.ID, hookName) + } +} + func testLaunchConfigurationName(n string, lc *autoscaling.LaunchConfiguration) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -1222,3 +1273,32 @@ resource "aws_security_group" "tf_test_self" { } } ` + +func testAccAWSAutoScalingGroupWithHookConfig(name string) string { + return fmt.Sprintf(` +resource "aws_launch_configuration" "foobar" { + image_id = "ami-21f78e11" + instance_type = "t1.micro" +} + +resource "aws_autoscaling_group" "bar" { + availability_zones = ["us-west-2a"] + name = "%s" + max_size = 5 + min_size = 2 + health_check_type = "ELB" + desired_capacity = 4 + force_delete = true + termination_policies = ["OldestInstance","ClosestToNextInstanceHour"] + + launch_configuration = "${aws_launch_configuration.foobar.name}" + + initial_lifecycle_hook { + name = "launching" + default_result = "CONTINUE" + heartbeat_timeout = 30 # minimum value + lifecycle_transition = "autoscaling:EC2_INSTANCE_LAUNCHING" + } +} +`, name) +} diff --git a/builtin/providers/aws/resource_aws_autoscaling_group_waiting.go b/builtin/providers/aws/resource_aws_autoscaling_group_waiting.go index bfafa9b33..80f126983 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_waiting.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_waiting.go @@ -93,8 +93,8 @@ func waitForASGCapacity( type capacitySatisfiedFunc func(*schema.ResourceData, int, int) (bool, string) -// capacitySatifiedCreate treats all targets as minimums -func capacitySatifiedCreate(d *schema.ResourceData, haveASG, haveELB int) (bool, string) { +// capacitySatisfiedCreate treats all targets as minimums +func capacitySatisfiedCreate(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 @@ -114,8 +114,8 @@ func capacitySatifiedCreate(d *schema.ResourceData, haveASG, haveELB int) (bool, return true, "" } -// capacitySatifiedUpdate only cares about specific targets -func capacitySatifiedUpdate(d *schema.ResourceData, haveASG, haveELB int) (bool, string) { +// capacitySatisfiedUpdate only cares about specific targets +func capacitySatisfiedUpdate(d *schema.ResourceData, haveASG, haveELB int) (bool, string) { if wantASG := d.Get("desired_capacity").(int); wantASG > 0 { if haveASG != wantASG { return false, fmt.Sprintf( 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 d7c3895c0..fa27102e8 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_waiting_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_waiting_test.go @@ -127,7 +127,7 @@ func TestCapacitySatisfiedCreate(t *testing.T) { t.Fatalf("err: %s", err) } } - gotSatisfied, gotReason := capacitySatifiedCreate(d, tc.HaveASG, tc.HaveELB) + gotSatisfied, gotReason := capacitySatisfiedCreate(d, tc.HaveASG, tc.HaveELB) if gotSatisfied != tc.ExpectSatisfied { t.Fatalf("%s: expected satisfied: %t, got: %t (reason: %s)", @@ -209,7 +209,7 @@ func TestCapacitySatisfiedUpdate(t *testing.T) { t.Fatalf("err: %s", err) } } - gotSatisfied, gotReason := capacitySatifiedUpdate(d, tc.HaveASG, tc.HaveELB) + gotSatisfied, gotReason := capacitySatisfiedUpdate(d, tc.HaveASG, tc.HaveELB) if gotSatisfied != tc.ExpectSatisfied { t.Fatalf("%s: expected satisfied: %t, got: %t (reason: %s)", diff --git a/builtin/providers/aws/resource_aws_autoscaling_lifecycle_hook.go b/builtin/providers/aws/resource_aws_autoscaling_lifecycle_hook.go index 8b3a3159d..7743c1ad3 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_lifecycle_hook.go +++ b/builtin/providers/aws/resource_aws_autoscaling_lifecycle_hook.go @@ -21,37 +21,37 @@ func resourceAwsAutoscalingLifecycleHook() *schema.Resource { Delete: resourceAwsAutoscalingLifecycleHookDelete, Schema: map[string]*schema.Schema{ - "name": &schema.Schema{ + "name": { Type: schema.TypeString, Required: true, ForceNew: true, }, - "autoscaling_group_name": &schema.Schema{ + "autoscaling_group_name": { Type: schema.TypeString, Required: true, }, - "default_result": &schema.Schema{ + "default_result": { Type: schema.TypeString, Optional: true, Computed: true, }, - "heartbeat_timeout": &schema.Schema{ + "heartbeat_timeout": { Type: schema.TypeInt, Optional: true, }, - "lifecycle_transition": &schema.Schema{ + "lifecycle_transition": { Type: schema.TypeString, Required: true, }, - "notification_metadata": &schema.Schema{ + "notification_metadata": { Type: schema.TypeString, Optional: true, }, - "notification_target_arn": &schema.Schema{ + "notification_target_arn": { Type: schema.TypeString, Optional: true, }, - "role_arn": &schema.Schema{ + "role_arn": { Type: schema.TypeString, Optional: true, }, @@ -59,13 +59,10 @@ func resourceAwsAutoscalingLifecycleHook() *schema.Resource { } } -func resourceAwsAutoscalingLifecycleHookPut(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*AWSClient).autoscalingconn - params := getAwsAutoscalingPutLifecycleHookInput(d) - +func resourceAwsAutoscalingLifecycleHookPutOp(conn *autoscaling.AutoScaling, params *autoscaling.PutLifecycleHookInput) error { log.Printf("[DEBUG] AutoScaling PutLifecyleHook: %s", params) - err := resource.Retry(5*time.Minute, func() *resource.RetryError { - _, err := conn.PutLifecycleHook(¶ms) + return resource.Retry(5*time.Minute, func() *resource.RetryError { + _, err := conn.PutLifecycleHook(params) if err != nil { if awsErr, ok := err.(awserr.Error); ok { @@ -77,8 +74,13 @@ func resourceAwsAutoscalingLifecycleHookPut(d *schema.ResourceData, meta interfa } return nil }) +} - if err != nil { +func resourceAwsAutoscalingLifecycleHookPut(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + params := getAwsAutoscalingPutLifecycleHookInput(d) + + if err := resourceAwsAutoscalingLifecycleHookPutOp(conn, ¶ms); err != nil { return err } diff --git a/builtin/providers/aws/resource_aws_autoscaling_lifecycle_hook_test.go b/builtin/providers/aws/resource_aws_autoscaling_lifecycle_hook_test.go index 8f1bbe04f..ad5a7a8a7 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_lifecycle_hook_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_lifecycle_hook_test.go @@ -57,23 +57,28 @@ func testAccCheckLifecycleHookExists(n string) resource.TestCheckFunc { return fmt.Errorf("Not found: %s", n) } - conn := testAccProvider.Meta().(*AWSClient).autoscalingconn - params := &autoscaling.DescribeLifecycleHooksInput{ - AutoScalingGroupName: aws.String(rs.Primary.Attributes["autoscaling_group_name"]), - LifecycleHookNames: []*string{aws.String(rs.Primary.ID)}, - } - resp, err := conn.DescribeLifecycleHooks(params) - if err != nil { - return err - } - if len(resp.LifecycleHooks) == 0 { - return fmt.Errorf("LifecycleHook not found") - } - - return nil + return checkLifecycleHookExistsByName( + rs.Primary.Attributes["autoscaling_group_name"], rs.Primary.ID) } } +func checkLifecycleHookExistsByName(asgName, hookName string) error { + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + params := &autoscaling.DescribeLifecycleHooksInput{ + AutoScalingGroupName: aws.String(asgName), + LifecycleHookNames: []*string{aws.String(hookName)}, + } + resp, err := conn.DescribeLifecycleHooks(params) + if err != nil { + return err + } + if len(resp.LifecycleHooks) == 0 { + return fmt.Errorf("LifecycleHook not found") + } + + return nil +} + func testAccCheckAWSAutoscalingLifecycleHookDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).autoscalingconn