diff --git a/builtin/providers/aws/autoscaling_tags.go b/builtin/providers/aws/autoscaling_tags.go index 0ca063598b..5c09115053 100644 --- a/builtin/providers/aws/autoscaling_tags.go +++ b/builtin/providers/aws/autoscaling_tags.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "regexp" + "strconv" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/autoscaling" @@ -12,8 +13,8 @@ import ( "github.com/hashicorp/terraform/helper/schema" ) -// tagsSchema returns the schema to use for tags. -func autoscalingTagsSchema() *schema.Schema { +// autoscalingTagSchema returns the schema to use for the tag element. +func autoscalingTagSchema() *schema.Schema { return &schema.Schema{ Type: schema.TypeSet, Optional: true, @@ -35,11 +36,11 @@ func autoscalingTagsSchema() *schema.Schema { }, }, }, - Set: autoscalingTagsToHash, + Set: autoscalingTagToHash, } } -func autoscalingTagsToHash(v interface{}) int { +func autoscalingTagToHash(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) buf.WriteString(fmt.Sprintf("%s-", m["key"].(string))) @@ -52,35 +53,74 @@ func autoscalingTagsToHash(v interface{}) int { // setTags is a helper to set the tags for a resource. It expects the // tags field to be named "tag" func setAutoscalingTags(conn *autoscaling.AutoScaling, d *schema.ResourceData) error { - if d.HasChange("tag") { + resourceID := d.Get("name").(string) + var createTags, removeTags []*autoscaling.Tag + + if d.HasChange("tag") || d.HasChange("tags") { oraw, nraw := d.GetChange("tag") o := setToMapByKey(oraw.(*schema.Set), "key") n := setToMapByKey(nraw.(*schema.Set), "key") - resourceID := d.Get("name").(string) - c, r := diffAutoscalingTags( - autoscalingTagsFromMap(o, resourceID), - autoscalingTagsFromMap(n, resourceID), - resourceID) - create := autoscaling.CreateOrUpdateTagsInput{ - Tags: c, - } - remove := autoscaling.DeleteTagsInput{ - Tags: r, + old, err := autoscalingTagsFromMap(o, resourceID) + if err != nil { + return err } - // Set tags - if len(r) > 0 { - log.Printf("[DEBUG] Removing autoscaling tags: %#v", r) - if _, err := conn.DeleteTags(&remove); err != nil { - return err - } + new, err := autoscalingTagsFromMap(n, resourceID) + if err != nil { + return err } - if len(c) > 0 { - log.Printf("[DEBUG] Creating autoscaling tags: %#v", c) - if _, err := conn.CreateOrUpdateTags(&create); err != nil { - return err - } + + c, r, err := diffAutoscalingTags(old, new, resourceID) + if err != nil { + return err + } + + createTags = append(createTags, c...) + removeTags = append(removeTags, r...) + + oraw, nraw = d.GetChange("tags") + old, err = autoscalingTagsFromList(oraw.([]interface{}), resourceID) + if err != nil { + return err + } + + new, err = autoscalingTagsFromList(nraw.([]interface{}), resourceID) + if err != nil { + return err + } + + c, r, err = diffAutoscalingTags(old, new, resourceID) + if err != nil { + return err + } + + createTags = append(createTags, c...) + removeTags = append(removeTags, r...) + } + + // Set tags + if len(removeTags) > 0 { + log.Printf("[DEBUG] Removing autoscaling tags: %#v", removeTags) + + remove := autoscaling.DeleteTagsInput{ + Tags: removeTags, + } + + if _, err := conn.DeleteTags(&remove); err != nil { + return err + } + } + + if len(createTags) > 0 { + log.Printf("[DEBUG] Creating autoscaling tags: %#v", createTags) + + create := autoscaling.CreateOrUpdateTagsInput{ + Tags: createTags, + } + + if _, err := conn.CreateOrUpdateTags(&create); err != nil { + return err } } @@ -90,11 +130,12 @@ func setAutoscalingTags(conn *autoscaling.AutoScaling, d *schema.ResourceData) e // diffTags takes our tags locally and the ones remotely and returns // the set of tags that must be created, and the set of tags that must // be destroyed. -func diffAutoscalingTags(oldTags, newTags []*autoscaling.Tag, resourceID string) ([]*autoscaling.Tag, []*autoscaling.Tag) { +func diffAutoscalingTags(oldTags, newTags []*autoscaling.Tag, resourceID string) ([]*autoscaling.Tag, []*autoscaling.Tag, error) { // First, we're creating everything we have create := make(map[string]interface{}) for _, t := range newTags { tag := map[string]interface{}{ + "key": *t.Key, "value": *t.Value, "propagate_at_launch": *t.PropagateAtLaunch, } @@ -112,27 +153,99 @@ func diffAutoscalingTags(oldTags, newTags []*autoscaling.Tag, resourceID string) } } - return autoscalingTagsFromMap(create, resourceID), remove + createTags, err := autoscalingTagsFromMap(create, resourceID) + if err != nil { + return nil, nil, err + } + + return createTags, remove, nil +} + +func autoscalingTagsFromList(vs []interface{}, resourceID string) ([]*autoscaling.Tag, error) { + result := make([]*autoscaling.Tag, 0, len(vs)) + for _, tag := range vs { + attr, ok := tag.(map[string]interface{}) + if !ok { + continue + } + + t, err := autoscalingTagFromMap(attr, resourceID) + if err != nil { + return nil, err + } + + if t != nil { + result = append(result, t) + } + } + return result, nil } // tagsFromMap returns the tags for the given map of data. -func autoscalingTagsFromMap(m map[string]interface{}, resourceID string) []*autoscaling.Tag { +func autoscalingTagsFromMap(m map[string]interface{}, resourceID string) ([]*autoscaling.Tag, error) { result := make([]*autoscaling.Tag, 0, len(m)) - for k, v := range m { - attr := v.(map[string]interface{}) - t := &autoscaling.Tag{ - Key: aws.String(k), - Value: aws.String(attr["value"].(string)), - PropagateAtLaunch: aws.Bool(attr["propagate_at_launch"].(bool)), - ResourceId: aws.String(resourceID), - ResourceType: aws.String("auto-scaling-group"), + for _, v := range m { + attr, ok := v.(map[string]interface{}) + if !ok { + continue } - if !tagIgnoredAutoscaling(t) { + + t, err := autoscalingTagFromMap(attr, resourceID) + if err != nil { + return nil, err + } + + if t != nil { result = append(result, t) } } - return result + return result, nil +} + +func autoscalingTagFromMap(attr map[string]interface{}, resourceID string) (*autoscaling.Tag, error) { + if _, ok := attr["key"]; !ok { + return nil, fmt.Errorf("%s: invalid tag attributes: key missing", resourceID) + } + + if _, ok := attr["value"]; !ok { + return nil, fmt.Errorf("%s: invalid tag attributes: value missing", resourceID) + } + + if _, ok := attr["propagate_at_launch"]; !ok { + return nil, fmt.Errorf("%s: invalid tag attributes: propagate_at_launch missing", resourceID) + } + + var propagateAtLaunch bool + var err error + + if v, ok := attr["propagate_at_launch"].(bool); ok { + propagateAtLaunch = v + } + + if v, ok := attr["propagate_at_launch"].(string); ok { + if propagateAtLaunch, err = strconv.ParseBool(v); err != nil { + return nil, fmt.Errorf( + "%s: invalid tag attribute: invalid value for propagate_at_launch: %s", + resourceID, + v, + ) + } + } + + t := &autoscaling.Tag{ + Key: aws.String(attr["key"].(string)), + Value: aws.String(attr["value"].(string)), + PropagateAtLaunch: aws.Bool(propagateAtLaunch), + ResourceId: aws.String(resourceID), + ResourceType: aws.String("auto-scaling-group"), + } + + if tagIgnoredAutoscaling(t) { + return nil, nil + } + + return t, nil } // autoscalingTagsToMap turns the list of tags into a map. @@ -140,6 +253,7 @@ func autoscalingTagsToMap(ts []*autoscaling.Tag) map[string]interface{} { tags := make(map[string]interface{}) for _, t := range ts { tag := map[string]interface{}{ + "key": *t.Key, "value": *t.Value, "propagate_at_launch": *t.PropagateAtLaunch, } @@ -154,6 +268,7 @@ func autoscalingTagDescriptionsToMap(ts *[]*autoscaling.TagDescription) map[stri tags := make(map[string]map[string]interface{}) for _, t := range *ts { tag := map[string]interface{}{ + "key": *t.Key, "value": *t.Value, "propagate_at_launch": *t.PropagateAtLaunch, } diff --git a/builtin/providers/aws/autoscaling_tags_test.go b/builtin/providers/aws/autoscaling_tags_test.go index 04d8c15cba..0107764d14 100644 --- a/builtin/providers/aws/autoscaling_tags_test.go +++ b/builtin/providers/aws/autoscaling_tags_test.go @@ -20,24 +20,28 @@ func TestDiffAutoscalingTags(t *testing.T) { { Old: map[string]interface{}{ "Name": map[string]interface{}{ + "key": "Name", "value": "bar", "propagate_at_launch": true, }, }, New: map[string]interface{}{ "DifferentTag": map[string]interface{}{ + "key": "DifferentTag", "value": "baz", "propagate_at_launch": true, }, }, Create: map[string]interface{}{ "DifferentTag": map[string]interface{}{ + "key": "DifferentTag", "value": "baz", "propagate_at_launch": true, }, }, Remove: map[string]interface{}{ "Name": map[string]interface{}{ + "key": "Name", "value": "bar", "propagate_at_launch": true, }, @@ -48,24 +52,28 @@ func TestDiffAutoscalingTags(t *testing.T) { { Old: map[string]interface{}{ "Name": map[string]interface{}{ + "key": "Name", "value": "bar", "propagate_at_launch": true, }, }, New: map[string]interface{}{ "Name": map[string]interface{}{ + "key": "Name", "value": "baz", "propagate_at_launch": false, }, }, Create: map[string]interface{}{ "Name": map[string]interface{}{ + "key": "Name", "value": "baz", "propagate_at_launch": false, }, }, Remove: map[string]interface{}{ "Name": map[string]interface{}{ + "key": "Name", "value": "bar", "propagate_at_launch": true, }, @@ -76,10 +84,20 @@ func TestDiffAutoscalingTags(t *testing.T) { var resourceID = "sample" for i, tc := range cases { - awsTagsOld := autoscalingTagsFromMap(tc.Old, resourceID) - awsTagsNew := autoscalingTagsFromMap(tc.New, resourceID) + awsTagsOld, err := autoscalingTagsFromMap(tc.Old, resourceID) + if err != nil { + t.Fatalf("%d: unexpected error convertig old tags: %v", i, err) + } - c, r := diffAutoscalingTags(awsTagsOld, awsTagsNew, resourceID) + awsTagsNew, err := autoscalingTagsFromMap(tc.New, resourceID) + if err != nil { + t.Fatalf("%d: unexpected error convertig new tags: %v", i, err) + } + + c, r, err := diffAutoscalingTags(awsTagsOld, awsTagsNew, resourceID) + if err != nil { + t.Fatalf("%d: unexpected error diff'ing tags: %v", i, err) + } cm := autoscalingTagsToMap(c) rm := autoscalingTagsToMap(r) diff --git a/builtin/providers/aws/import_aws_autoscaling_group_test.go b/builtin/providers/aws/import_aws_autoscaling_group_test.go index 920df152cf..666563b506 100644 --- a/builtin/providers/aws/import_aws_autoscaling_group_test.go +++ b/builtin/providers/aws/import_aws_autoscaling_group_test.go @@ -18,7 +18,7 @@ func TestAccAWSAutoScalingGroup_importBasic(t *testing.T) { CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccAWSAutoScalingGroupConfig(randName), + Config: testAccAWSAutoScalingGroupImport(randName), }, resource.TestStep{ diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go index ef21f2492c..be23aa02f7 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group.go @@ -244,7 +244,14 @@ func resourceAwsAutoscalingGroup() *schema.Resource { }, }, - "tag": autoscalingTagsSchema(), + "tag": autoscalingTagSchema(), + + "tags": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeMap}, + ConflictsWith: []string{"tag"}, + }, }, } } @@ -344,9 +351,23 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) createOpts.AvailabilityZones = expandStringList(v.(*schema.Set).List()) } + resourceID := d.Get("name").(string) if v, ok := d.GetOk("tag"); ok { - createOpts.Tags = autoscalingTagsFromMap( - setToMapByKey(v.(*schema.Set), "key"), d.Get("name").(string)) + var err error + createOpts.Tags, err = autoscalingTagsFromMap( + setToMapByKey(v.(*schema.Set), "key"), resourceID) + if err != nil { + return err + } + } + + if v, ok := d.GetOk("tags"); ok { + tags, err := autoscalingTagsFromList(v.([]interface{}), resourceID) + if err != nil { + return err + } + + createOpts.Tags = append(createOpts.Tags, tags...) } if v, ok := d.GetOk("default_cooldown"); ok { @@ -457,7 +478,49 @@ 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("tag", autoscalingTagDescriptionsToSlice(g.Tags)) + + var tagList, tagsList []*autoscaling.TagDescription + var tagOk, tagsOk bool + var v interface{} + + if v, tagOk = d.GetOk("tag"); tagOk { + tags := setToMapByKey(v.(*schema.Set), "key") + for _, t := range g.Tags { + if _, ok := tags[*t.Key]; ok { + tagList = append(tagList, t) + } + } + d.Set("tag", autoscalingTagDescriptionsToSlice(tagList)) + } + + if v, tagsOk = d.GetOk("tags"); tagsOk { + tags := map[string]struct{}{} + for _, tag := range v.([]interface{}) { + attr, ok := tag.(map[string]interface{}) + if !ok { + continue + } + + key, ok := attr["key"].(string) + if !ok { + continue + } + + tags[key] = struct{}{} + } + + for _, t := range g.Tags { + if _, ok := tags[*t.Key]; ok { + tagsList = append(tagsList, t) + } + } + d.Set("tags", autoscalingTagDescriptionsToSlice(tagsList)) + } + + if !tagOk && !tagsOk { + d.Set("tag", autoscalingTagDescriptionsToSlice(g.Tags)) + } + d.Set("vpc_zone_identifier", strings.Split(*g.VPCZoneIdentifier, ",")) d.Set("protect_from_scale_in", g.NewInstancesProtectedFromScaleIn) @@ -549,10 +612,16 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) if err := setAutoscalingTags(conn, d); err != nil { return err - } else { + } + + if d.HasChange("tag") { d.SetPartial("tag") } + if d.HasChange("tags") { + d.SetPartial("tags") + } + log.Printf("[DEBUG] AutoScaling Group update configuration: %#v", opts) _, err := conn.UpdateAutoScalingGroup(&opts) if err != nil { diff --git a/builtin/providers/aws/resource_aws_autoscaling_group_test.go b/builtin/providers/aws/resource_aws_autoscaling_group_test.go index ee17f88444..b60694a7bc 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_test.go @@ -74,8 +74,16 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) { resource.TestCheckResourceAttr( "aws_autoscaling_group.bar", "protect_from_scale_in", "true"), testLaunchConfigurationName("aws_autoscaling_group.bar", &lc), - testAccCheckAutoscalingTags(&group.Tags, "Bar", map[string]interface{}{ - "value": "bar-foo", + testAccCheckAutoscalingTags(&group.Tags, "FromTags1Changed", map[string]interface{}{ + "value": "value1changed", + "propagate_at_launch": true, + }), + testAccCheckAutoscalingTags(&group.Tags, "FromTags2", map[string]interface{}{ + "value": "value2changed", + "propagate_at_launch": true, + }), + testAccCheckAutoscalingTags(&group.Tags, "FromTags3", map[string]interface{}{ + "value": "value3", "propagate_at_launch": true, }), ), @@ -185,8 +193,16 @@ func TestAccAWSAutoScalingGroup_tags(t *testing.T) { Config: testAccAWSAutoScalingGroupConfig(randName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group), - testAccCheckAutoscalingTags(&group.Tags, "Foo", map[string]interface{}{ - "value": "foo-bar", + testAccCheckAutoscalingTags(&group.Tags, "FromTags1", map[string]interface{}{ + "value": "value1", + "propagate_at_launch": true, + }), + testAccCheckAutoscalingTags(&group.Tags, "FromTags2", map[string]interface{}{ + "value": "value2", + "propagate_at_launch": true, + }), + testAccCheckAutoscalingTags(&group.Tags, "FromTags3", map[string]interface{}{ + "value": "value3", "propagate_at_launch": true, }), ), @@ -197,8 +213,16 @@ func TestAccAWSAutoScalingGroup_tags(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group), testAccCheckAutoscalingTagNotExists(&group.Tags, "Foo"), - testAccCheckAutoscalingTags(&group.Tags, "Bar", map[string]interface{}{ - "value": "bar-foo", + testAccCheckAutoscalingTags(&group.Tags, "FromTags1Changed", map[string]interface{}{ + "value": "value1changed", + "propagate_at_launch": true, + }), + testAccCheckAutoscalingTags(&group.Tags, "FromTags2", map[string]interface{}{ + "value": "value2changed", + "propagate_at_launch": true, + }), + testAccCheckAutoscalingTags(&group.Tags, "FromTags3", map[string]interface{}{ + "value": "value3", "propagate_at_launch": true, }), ), @@ -572,8 +596,8 @@ func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.Group, name st } t := &autoscaling.TagDescription{ - Key: aws.String("Foo"), - Value: aws.String("foo-bar"), + Key: aws.String("FromTags1"), + Value: aws.String("value1"), PropagateAtLaunch: aws.Bool(true), ResourceType: aws.String("auto-scaling-group"), ResourceId: group.AutoScalingGroupName, @@ -850,11 +874,23 @@ resource "aws_autoscaling_group" "bar" { launch_configuration = "${aws_launch_configuration.foobar.name}" - tag { - key = "Foo" - value = "foo-bar" - propagate_at_launch = true - } + tags = [ + { + key = "FromTags1" + value = "value1" + propagate_at_launch = true + }, + { + key = "FromTags2" + value = "value2" + propagate_at_launch = true + }, + { + key = "FromTags3" + value = "value3" + propagate_at_launch = true + }, + ] } `, name, name) } @@ -885,13 +921,70 @@ resource "aws_autoscaling_group" "bar" { launch_configuration = "${aws_launch_configuration.new.name}" + tags = [ + { + key = "FromTags1Changed" + value = "value1changed" + propagate_at_launch = true + }, + { + key = "FromTags2" + value = "value2changed" + propagate_at_launch = true + }, + { + key = "FromTags3" + value = "value3" + propagate_at_launch = true + }, + ] +} +`, name) +} + +func testAccAWSAutoScalingGroupImport(name string) string { + return fmt.Sprintf(` +resource "aws_launch_configuration" "foobar" { + image_id = "ami-21f78e11" + instance_type = "t1.micro" +} + +resource "aws_placement_group" "test" { + name = "asg_pg_%s" + strategy = "cluster" +} + +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}" + tag { - key = "Bar" - value = "bar-foo" + key = "FromTags1" + value = "value1" + propagate_at_launch = true + } + + tag { + key = "FromTags2" + value = "value2" + propagate_at_launch = true + } + + tag { + key = "FromTags3" + value = "value3" propagate_at_launch = true } } -`, name) +`, name, name) } const testAccAWSAutoScalingGroupConfigWithLoadBalancer = ` 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 86b92a6699..21bafe7162 100644 --- a/website/source/docs/providers/aws/r/autoscaling_group.html.markdown +++ b/website/source/docs/providers/aws/r/autoscaling_group.html.markdown @@ -60,6 +60,54 @@ EOF } ``` +## Interpolated tags + +``` +variable extra_tags { + default = [ + { + key = "Foo" + value = "Bar" + propagate_at_launch = true + }, + { + key = "Baz" + value = "Bam" + propagate_at_launch = true + }, + ] +} + +resource "aws_autoscaling_group" "bar" { + availability_zones = ["us-east-1a"] + name = "foobar3-terraform-test" + max_size = 5 + min_size = 2 + launch_configuration = "${aws_launch_configuration.foobar.name}" + + tags = [ + { + key = "explicit1" + value = "value1" + propagate_at_launch = true + }, + { + key = "explicit2" + value = "value2" + propagate_at_launch = true + }, + ] + + tags = ["${concat( + list( + map("key", "interpolation1", "value", "value3", "propagate_at_launch", true), + map("key", "interpolation2", "value", "value4", "propagate_at_launch", true) + ), + var.extra_tags) + }"] +} +``` + ## Argument Reference The following arguments are supported: @@ -100,6 +148,7 @@ Application Load Balancing * `suspended_processes` - (Optional) A list of processes to suspend for the AutoScaling Group. The allowed values are `Launch`, `Terminate`, `HealthCheck`, `ReplaceUnhealthy`, `AZRebalance`, `AlarmNotification`, `ScheduledActions`, `AddToLoadBalancer`. Note that if you suspend either the `Launch` or `Terminate` process types, it can prevent your autoscaling group from functioning properly. * `tag` (Optional) A list of tag blocks. Tags documented below. +* `tags` (Optional) A list of tag blocks (maps). Tags documented below. * `placement_group` (Optional) The name of the placement group into which you'll launch your instances, if any. * `metrics_granularity` - (Optional) The granularity to associate with the metrics to collect. The only valid value is `1Minute`. Default is `1Minute`. * `enabled_metrics` - (Optional) A list of metrics to collect. The allowed values are `GroupMinSize`, `GroupMaxSize`, `GroupDesiredCapacity`, `GroupInServiceInstances`, `GroupPendingInstances`, `GroupStandbyInstances`, `GroupTerminatingInstances`, `GroupTotalInstances`. @@ -123,11 +172,18 @@ Note that if you suspend either the `Launch` or `Terminate` process types, it ca Tags support the following: +The `tag` attribute accepts exactly one tag declaration with the following fields: + * `key` - (Required) Key * `value` - (Required) Value * `propagate_at_launch` - (Required) Enables propagation of the tag to Amazon EC2 instances launched via this ASG +To declare multiple tags additional `tag` blocks can be specified. +Alternatively the `tags` attributes can be used, which accepts a list of maps containing the above field names as keys and their respective values. +This allows the construction of dynamic lists of tags which is not possible using the single `tag` attribute. +`tag` and `tags` are mutually exclusive, only one of them can be specified. + ## Attributes Reference The following attributes are exported: