providers/aws: add tags for resource_aws_autoscaling_group (#13574)

The existing "tag" field on autoscaling groups is very limited in that it
cannot be used in conjunction with interpolation preventing from adding
dynamic tag entries.

Other AWS resources don't have this restriction on tags because they work
directly on the map type.

AWS autoscaling groups on the other hand have an additional field
"propagate_at_launch" which is not usable with a pure map type.

This fixes it by introducing an additional field called "tags" which
allows specifying a list of maps. This preserves the possibility to
declare tags as with the "tag" field but additionally allows to
construct lists of maps using interpolation syntax.
This commit is contained in:
Sergiusz Urbaniak 2017-05-16 10:39:41 +02:00 committed by Radek Simko
parent 944fb6b645
commit 399830f1b7
6 changed files with 415 additions and 64 deletions

View File

@ -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,
}

View File

@ -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)

View File

@ -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{

View File

@ -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 {

View File

@ -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 = `

View File

@ -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: