Skip to content

Commit

Permalink
providers/aws: support for lifecycle hooks at ASG creation
Browse files Browse the repository at this point in the history
Closes #5619.
  • Loading branch information
Pierre Carrier authored and glasser committed Sep 1, 2016
1 parent b06fe6e commit 2410294
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 60 deletions.
166 changes: 140 additions & 26 deletions builtin/providers/aws/resource_aws_autoscaling_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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") {
Expand Down
80 changes: 80 additions & 0 deletions builtin/providers/aws/resource_aws_autoscaling_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down Expand Up @@ -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)",
Expand Down
Loading

0 comments on commit 2410294

Please sign in to comment.