diff --git a/builtin/providers/aws/autoscaling_tags.go b/builtin/providers/aws/autoscaling_tags.go new file mode 100644 index 0000000000..508a0ddd8c --- /dev/null +++ b/builtin/providers/aws/autoscaling_tags.go @@ -0,0 +1,170 @@ +package aws + +import ( + "bytes" + "fmt" + "log" + + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/autoscaling" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +// tagsSchema returns the schema to use for tags. +func autoscalingTagsSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "value": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "propagate_at_launch": &schema.Schema{ + Type: schema.TypeBool, + Required: true, + }, + }, + }, + Set: autoscalingTagsToHash, + } +} + +func autoscalingTagsToHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["key"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["value"].(string))) + buf.WriteString(fmt.Sprintf("%t-", m["propagate_at_launch"].(bool))) + + return hashcode.String(buf.String()) +} + +// 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") { + 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.CreateOrUpdateTagsType{ + Tags: c, + } + remove := autoscaling.DeleteTagsType{ + Tags: r, + } + + // Set tags + if len(r) > 0 { + log.Printf("[DEBUG] Removing autoscaling tags: %#v", r) + if err := conn.DeleteTags(&remove); 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 + } + } + } + + return nil +} + +// 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) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + tag := map[string]interface{}{ + "value": *t.Value, + "propagate_at_launch": *t.PropagateAtLaunch, + } + create[*t.Key] = tag + } + + // Build the list of what to remove + var remove []autoscaling.Tag + for _, t := range oldTags { + old, ok := create[*t.Key].(map[string]interface{}) + + if !ok || old["value"] != *t.Value || old["propagate_at_launch"] != *t.PropagateAtLaunch { + // Delete it! + remove = append(remove, t) + } + } + + return autoscalingTagsFromMap(create, resourceID), remove +} + +// tagsFromMap returns the tags for the given map of data. +func autoscalingTagsFromMap(m map[string]interface{}, resourceID string) []autoscaling.Tag { + result := make([]autoscaling.Tag, 0, len(m)) + for k, v := range m { + attr := v.(map[string]interface{}) + result = append(result, autoscaling.Tag{ + Key: aws.String(k), + Value: aws.String(attr["value"].(string)), + PropagateAtLaunch: aws.Boolean(attr["propagate_at_launch"].(bool)), + ResourceID: aws.String(resourceID), + ResourceType: aws.String("auto-scaling-group"), + }) + } + + return result +} + +// autoscalingTagsToMap turns the list of tags into a map. +func autoscalingTagsToMap(ts []autoscaling.Tag) map[string]interface{} { + tags := make(map[string]interface{}) + for _, t := range ts { + tag := map[string]interface{}{ + "value": *t.Value, + "propagate_at_launch": *t.PropagateAtLaunch, + } + tags[*t.Key] = tag + } + + return tags +} + +// autoscalingTagDescriptionsToMap turns the list of tags into a map. +func autoscalingTagDescriptionsToMap(ts []autoscaling.TagDescription) map[string]map[string]interface{} { + tags := make(map[string]map[string]interface{}) + for _, t := range ts { + tag := map[string]interface{}{ + "value": t.Value, + "propagate_at_launch": t.PropagateAtLaunch, + } + tags[*t.Key] = tag + } + + return tags +} + +func setToMapByKey(s *schema.Set, key string) map[string]interface{} { + result := make(map[string]interface{}) + for _, rawData := range s.List() { + data := rawData.(map[string]interface{}) + result[data[key].(string)] = data + } + + return result +} diff --git a/builtin/providers/aws/autoscaling_tags_test.go b/builtin/providers/aws/autoscaling_tags_test.go new file mode 100644 index 0000000000..7d61e3b18a --- /dev/null +++ b/builtin/providers/aws/autoscaling_tags_test.go @@ -0,0 +1,122 @@ +package aws + +import ( + "fmt" + "reflect" + "testing" + + "github.com/hashicorp/aws-sdk-go/gen/autoscaling" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestDiffAutoscalingTags(t *testing.T) { + cases := []struct { + Old, New map[string]interface{} + Create, Remove map[string]interface{} + }{ + // Basic add/remove + { + Old: map[string]interface{}{ + "Name": map[string]interface{}{ + "value": "bar", + "propagate_at_launch": true, + }, + }, + New: map[string]interface{}{ + "DifferentTag": map[string]interface{}{ + "value": "baz", + "propagate_at_launch": true, + }, + }, + Create: map[string]interface{}{ + "DifferentTag": map[string]interface{}{ + "value": "baz", + "propagate_at_launch": true, + }, + }, + Remove: map[string]interface{}{ + "Name": map[string]interface{}{ + "value": "bar", + "propagate_at_launch": true, + }, + }, + }, + + // Modify + { + Old: map[string]interface{}{ + "Name": map[string]interface{}{ + "value": "bar", + "propagate_at_launch": true, + }, + }, + New: map[string]interface{}{ + "Name": map[string]interface{}{ + "value": "baz", + "propagate_at_launch": false, + }, + }, + Create: map[string]interface{}{ + "Name": map[string]interface{}{ + "value": "baz", + "propagate_at_launch": false, + }, + }, + Remove: map[string]interface{}{ + "Name": map[string]interface{}{ + "value": "bar", + "propagate_at_launch": true, + }, + }, + }, + } + + var resourceID = "sample" + + for i, tc := range cases { + awsTagsOld := autoscalingTagsFromMap(tc.Old, resourceID) + awsTagsNew := autoscalingTagsFromMap(tc.New, resourceID) + + c, r := diffAutoscalingTags(awsTagsOld, awsTagsNew, resourceID) + + cm := autoscalingTagsToMap(c) + rm := autoscalingTagsToMap(r) + if !reflect.DeepEqual(cm, tc.Create) { + t.Fatalf("%d: bad create: \n%#v\n%#v", i, cm, tc.Create) + } + if !reflect.DeepEqual(rm, tc.Remove) { + t.Fatalf("%d: bad remove: \n%#v\n%#v", i, rm, tc.Remove) + } + } +} + +// testAccCheckTags can be used to check the tags on a resource. +func testAccCheckAutoscalingTags( + ts *[]autoscaling.TagDescription, key string, expected map[string]interface{}) resource.TestCheckFunc { + return func(s *terraform.State) error { + m := autoscalingTagDescriptionsToMap(*ts) + v, ok := m[key] + if !ok { + return fmt.Errorf("Missing tag: %s", key) + } + + if v["value"] != expected["value"].(string) || + v["propagate_at_launch"] != expected["propagate_at_launch"].(bool) { + return fmt.Errorf("%s: bad value: %s", key, v) + } + + return nil + } +} + +func testAccCheckAutoscalingTagNotExists(ts *[]autoscaling.TagDescription, key string) resource.TestCheckFunc { + return func(s *terraform.State) error { + m := autoscalingTagDescriptionsToMap(*ts) + if _, ok := m[key]; ok { + return fmt.Errorf("Tag exists when it should not: %s", key) + } + + return nil + } +} diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go index 2950e252d0..9e5dc60a59 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group.go @@ -118,6 +118,8 @@ func resourceAwsAutoscalingGroup() *schema.Resource { return hashcode.String(v.(string)) }, }, + + "tag": autoscalingTagsSchema(), }, } } @@ -133,6 +135,11 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) autoScalingGroupOpts.AvailabilityZones = expandStringList( d.Get("availability_zones").(*schema.Set).List()) + if v, ok := d.GetOk("tag"); ok { + autoScalingGroupOpts.Tags = autoscalingTagsFromMap( + setToMapByKey(v.(*schema.Set), "key"), d.Get("name").(string)) + } + if v, ok := d.GetOk("default_cooldown"); ok { autoScalingGroupOpts.DefaultCooldown = aws.Integer(v.(int)) } @@ -195,6 +202,7 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e d.Set("min_size", *g.MinSize) d.Set("max_size", *g.MaxSize) d.Set("name", *g.AutoScalingGroupName) + d.Set("tag", g.Tags) d.Set("vpc_zone_identifier", strings.Split(*g.VPCZoneIdentifier, ",")) d.Set("termination_policies", g.TerminationPolicies) @@ -224,6 +232,12 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) opts.MaxSize = aws.Integer(d.Get("max_size").(int)) } + if err := setAutoscalingTags(autoscalingconn, d); err != nil { + return err + } else { + d.SetPartial("tag") + } + log.Printf("[DEBUG] AutoScaling Group update configuration: %#v", opts) err := autoscalingconn.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 2a7e053be3..2763450f6c 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_test.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "reflect" "testing" "github.com/hashicorp/aws-sdk-go/aws" @@ -53,6 +54,42 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) { resource.TestCheckResourceAttr( "aws_autoscaling_group.bar", "desired_capacity", "5"), testLaunchConfigurationName("aws_autoscaling_group.bar", &lc), + resource.TestCheckResourceAttr( + "aws_autoscaling_group.bar", "tag", "xxx"), + ), + }, + }, + }) +} + +func TestAccAWSAutoScalingGroup_tags(t *testing.T) { + var group autoscaling.AutoScalingGroup + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSAutoScalingGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group), + testAccCheckAutoscalingTags(&group.Tags, "foo", map[string]interface{}{ + "value": "bar", + "propagate_at_launch": true, + }), + ), + }, + + resource.TestStep{ + Config: testAccCheckInstanceConfigTagsUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group), + testAccCheckAutoscalingTagNotExists(&group.Tags, "foo"), + testAccCheckAutoscalingTags(&group.Tags, "bar", map[string]interface{}{ + "value": "baz", + "propagate_at_launch": true, + }), ), }, }, @@ -145,6 +182,19 @@ func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.AutoScalingGro return fmt.Errorf("Bad launch configuration name: %s", *group.LaunchConfigurationName) } + t := autoscaling.Tag{ + Key: aws.String("Name"), + Value: aws.String("foo-bar"), + PropagateAtLaunch: aws.Boolean(true), + } + + if !reflect.DeepEqual(group.Tags[0], t) { + return fmt.Errorf( + "Got:\n\n%#v\n\nExpected:\n\n%#v\n", + group.Tags[0], + t) + } + return nil } } @@ -226,6 +276,12 @@ resource "aws_autoscaling_group" "bar" { termination_policies = ["OldestInstance"] launch_configuration = "${aws_launch_configuration.foobar.name}" + + tag { + key = "Name" + value = "foo-bar" + propagate_at_launch = true + } } ` @@ -253,6 +309,12 @@ resource "aws_autoscaling_group" "bar" { force_delete = true launch_configuration = "${aws_launch_configuration.new.name}" + + tag { + key = "Name" + value = "bar-foo" + propagate_at_launch = true + } } `