From fff05e2aadc6836fface159b2461445a4545b2f8 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 3 Mar 2015 22:36:25 +0000 Subject: [PATCH 1/3] Tags support added for AWS ASG --- builtin/providers/aws/autoscaling_tags.go | 170 ++++++++++++++++++ .../providers/aws/autoscaling_tags_test.go | 122 +++++++++++++ .../aws/resource_aws_autoscaling_group.go | 14 ++ .../resource_aws_autoscaling_group_test.go | 62 +++++++ 4 files changed, 368 insertions(+) create mode 100644 builtin/providers/aws/autoscaling_tags.go create mode 100644 builtin/providers/aws/autoscaling_tags_test.go diff --git a/builtin/providers/aws/autoscaling_tags.go b/builtin/providers/aws/autoscaling_tags.go new file mode 100644 index 000000000..508a0ddd8 --- /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 000000000..7d61e3b18 --- /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 2950e252d..9e5dc60a5 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 2a7e053be..2763450f6 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 + } } ` From 7950ace39940f2d92512e6729d1567307ab2a2dd Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Mon, 2 Mar 2015 23:40:47 +0000 Subject: [PATCH 2/3] Documentation for ASG Tags added --- .../providers/aws/r/autoscale.html.markdown | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/website/source/docs/providers/aws/r/autoscale.html.markdown b/website/source/docs/providers/aws/r/autoscale.html.markdown index bb592a819..fe0643b72 100644 --- a/website/source/docs/providers/aws/r/autoscale.html.markdown +++ b/website/source/docs/providers/aws/r/autoscale.html.markdown @@ -23,6 +23,17 @@ resource "aws_autoscaling_group" "bar" { desired_capacity = 4 force_delete = true launch_configuration = "${aws_launch_configuration.foobar.name}" + + tag { + key = "foo" + value = "bar" + propagate_at_launch = true + } + tag { + key = "lorem" + value = "ipsum" + propagate_at_launch = false + } } ``` @@ -44,6 +55,14 @@ The following arguments are supported: group names. * `vpc_zone_identifier` (Optional) A list of subnet IDs to launch resources in. * `termination_policies` (Optional) A list of policies to decide how the instances in the auto scale group should be terminated. +* `tag` (Optional) A list of tag blocks. Tags documented below. + +Tags support the following: + +* `key` - (Required) Key +* `value` - (Required) Value +* `propagate_at_launch` - (Required) Enables propagation of the tag to + Amazon EC2 instances launched via this ASG ## Attributes Reference From deda59b50e318485360ae3563815cf682c4f29d8 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Thu, 26 Mar 2015 14:49:15 -0500 Subject: [PATCH 3/3] minor code cleanups to get acceptance tests passing --- builtin/providers/aws/autoscaling_tags.go | 4 +-- .../aws/resource_aws_autoscaling_group.go | 16 +++++----- .../resource_aws_autoscaling_group_test.go | 30 +++++++++++-------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/builtin/providers/aws/autoscaling_tags.go b/builtin/providers/aws/autoscaling_tags.go index 508a0ddd8..342caae54 100644 --- a/builtin/providers/aws/autoscaling_tags.go +++ b/builtin/providers/aws/autoscaling_tags.go @@ -150,8 +150,8 @@ func autoscalingTagDescriptionsToMap(ts []autoscaling.TagDescription) map[string tags := make(map[string]map[string]interface{}) for _, t := range ts { tag := map[string]interface{}{ - "value": t.Value, - "propagate_at_launch": t.PropagateAtLaunch, + "value": *t.Value, + "propagate_at_launch": *t.PropagateAtLaunch, } tags[*t.Key] = tag } diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go index 9e5dc60a5..de3bbe9cc 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group.go @@ -193,15 +193,15 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e } d.Set("availability_zones", g.AvailabilityZones) - d.Set("default_cooldown", *g.DefaultCooldown) - d.Set("desired_capacity", *g.DesiredCapacity) - d.Set("health_check_grace_period", *g.HealthCheckGracePeriod) - d.Set("health_check_type", *g.HealthCheckType) - d.Set("launch_configuration", *g.LaunchConfigurationName) + d.Set("default_cooldown", g.DefaultCooldown) + d.Set("desired_capacity", g.DesiredCapacity) + d.Set("health_check_grace_period", g.HealthCheckGracePeriod) + d.Set("health_check_type", g.HealthCheckType) + d.Set("launch_configuration", g.LaunchConfigurationName) d.Set("load_balancers", g.LoadBalancerNames) - d.Set("min_size", *g.MinSize) - d.Set("max_size", *g.MaxSize) - d.Set("name", *g.AutoScalingGroupName) + 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) diff --git a/builtin/providers/aws/resource_aws_autoscaling_group_test.go b/builtin/providers/aws/resource_aws_autoscaling_group_test.go index 2763450f6..661e71fe8 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_test.go @@ -26,7 +26,7 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) { testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group), testAccCheckAWSAutoScalingGroupAttributes(&group), resource.TestCheckResourceAttr( - "aws_autoscaling_group.bar", "availability_zones.2487133097", "us-west-2a"), + "aws_autoscaling_group.bar", "availability_zones.1807834199", "us-west-2a"), resource.TestCheckResourceAttr( "aws_autoscaling_group.bar", "name", "foobar3-terraform-test"), resource.TestCheckResourceAttr( @@ -54,8 +54,10 @@ 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"), + testAccCheckAutoscalingTags(&group.Tags, "Bar", map[string]interface{}{ + "value": "bar-foo", + "propagate_at_launch": true, + }), ), }, }, @@ -74,20 +76,20 @@ func TestAccAWSAutoScalingGroup_tags(t *testing.T) { Config: testAccAWSAutoScalingGroupConfig, Check: resource.ComposeTestCheckFunc( testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group), - testAccCheckAutoscalingTags(&group.Tags, "foo", map[string]interface{}{ - "value": "bar", + testAccCheckAutoscalingTags(&group.Tags, "Foo", map[string]interface{}{ + "value": "foo-bar", "propagate_at_launch": true, }), ), }, resource.TestStep{ - Config: testAccCheckInstanceConfigTagsUpdate, + Config: testAccAWSAutoScalingGroupConfigUpdate, Check: resource.ComposeTestCheckFunc( testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group), - testAccCheckAutoscalingTagNotExists(&group.Tags, "foo"), - testAccCheckAutoscalingTags(&group.Tags, "bar", map[string]interface{}{ - "value": "baz", + testAccCheckAutoscalingTagNotExists(&group.Tags, "Foo"), + testAccCheckAutoscalingTags(&group.Tags, "Bar", map[string]interface{}{ + "value": "bar-foo", "propagate_at_launch": true, }), ), @@ -182,10 +184,12 @@ func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.AutoScalingGro return fmt.Errorf("Bad launch configuration name: %s", *group.LaunchConfigurationName) } - t := autoscaling.Tag{ - Key: aws.String("Name"), + t := autoscaling.TagDescription{ + Key: aws.String("Foo"), Value: aws.String("foo-bar"), PropagateAtLaunch: aws.Boolean(true), + ResourceType: aws.String("auto-scaling-group"), + ResourceID: group.AutoScalingGroupName, } if !reflect.DeepEqual(group.Tags[0], t) { @@ -278,7 +282,7 @@ resource "aws_autoscaling_group" "bar" { launch_configuration = "${aws_launch_configuration.foobar.name}" tag { - key = "Name" + key = "Foo" value = "foo-bar" propagate_at_launch = true } @@ -311,7 +315,7 @@ resource "aws_autoscaling_group" "bar" { launch_configuration = "${aws_launch_configuration.new.name}" tag { - key = "Name" + key = "Bar" value = "bar-foo" propagate_at_launch = true }