diff --git a/builtin/providers/aws/resource_aws_alb.go b/builtin/providers/aws/resource_aws_alb.go index b68ab0c85..a7bbbdfb4 100644 --- a/builtin/providers/aws/resource_aws_alb.go +++ b/builtin/providers/aws/resource_aws_alb.go @@ -233,6 +233,12 @@ func resourceAwsAlbRead(d *schema.ResourceData, meta interface{}) error { func resourceAwsAlbUpdate(d *schema.ResourceData, meta interface{}) error { elbconn := meta.(*AWSClient).elbv2conn + if !d.IsNewResource() { + if err := setElbV2Tags(elbconn, d); err != nil { + return errwrap.Wrapf("Error Modifying Tags on ALB: {{err}}", err) + } + } + attributes := make([]*elbv2.LoadBalancerAttribute, 0) if d.HasChange("access_logs") { @@ -310,29 +316,6 @@ func resourceAwsAlbDelete(d *schema.ResourceData, meta interface{}) error { return nil } -// tagsToMapELBv2 turns the list of tags into a map. -func tagsToMapELBv2(ts []*elbv2.Tag) map[string]string { - result := make(map[string]string) - for _, t := range ts { - result[*t.Key] = *t.Value - } - - return result -} - -// tagsFromMapELBv2 returns the tags for the given map of data. -func tagsFromMapELBv2(m map[string]interface{}) []*elbv2.Tag { - var result []*elbv2.Tag - for k, v := range m { - result = append(result, &elbv2.Tag{ - Key: aws.String(k), - Value: aws.String(v.(string)), - }) - } - - return result -} - // flattenSubnetsFromAvailabilityZones creates a slice of strings containing the subnet IDs // for the ALB based on the AvailabilityZones structure returned by the API. func flattenSubnetsFromAvailabilityZones(availabilityZones []*elbv2.AvailabilityZone) []string { diff --git a/builtin/providers/aws/resource_aws_alb_target_group.go b/builtin/providers/aws/resource_aws_alb_target_group.go index ddf0597c3..3fdb2ba1d 100644 --- a/builtin/providers/aws/resource_aws_alb_target_group.go +++ b/builtin/providers/aws/resource_aws_alb_target_group.go @@ -146,6 +146,8 @@ func resourceAwsAlbTargetGroup() *schema.Resource { }, }, }, + + "tags": tagsSchema(), }, } } @@ -258,6 +260,10 @@ func resourceAwsAlbTargetGroupRead(d *schema.ResourceData, meta interface{}) err func resourceAwsAlbTargetGroupUpdate(d *schema.ResourceData, meta interface{}) error { elbconn := meta.(*AWSClient).elbv2conn + if err := setElbV2Tags(elbconn, d); err != nil { + return errwrap.Wrapf("Error Modifying Tags on ALB Target Group: {{err}}", err) + } + if d.HasChange("health_check") { healthChecks := d.Get("health_check").([]interface{}) diff --git a/builtin/providers/aws/resource_aws_alb_target_group_test.go b/builtin/providers/aws/resource_aws_alb_target_group_test.go index 446757f62..bea9b0a08 100644 --- a/builtin/providers/aws/resource_aws_alb_target_group_test.go +++ b/builtin/providers/aws/resource_aws_alb_target_group_test.go @@ -51,6 +51,37 @@ func TestAccAWSALBTargetGroup_basic(t *testing.T) { }) } +func TestAccAWSALBTargetGroup_tags(t *testing.T) { + var conf elbv2.TargetGroup + targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_alb_target_group.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSALBTargetGroupConfig_basic(targetGroupName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), + resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.%", "1"), + resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.TestName", "TestAccAWSALBTargetGroup_basic"), + ), + }, + { + Config: testAccAWSALBTargetGroupConfig_updateTags(targetGroupName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), + resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.%", "2"), + resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.Environment", "Production"), + resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.Type", "ALB Target Group"), + ), + }, + }, + }) +} + func TestAccAWSALBTargetGroup_updateHealthCheck(t *testing.T) { var conf elbv2.TargetGroup targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) @@ -198,6 +229,50 @@ func testAccAWSALBTargetGroupConfig_basic(targetGroupName string) string { unhealthy_threshold = 3 matcher = "200-299" } + + tags { + TestName = "TestAccAWSALBTargetGroup_basic" + } +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags { + TestName = "TestAccAWSALBTargetGroup_basic" + } +}`, targetGroupName) +} + +func testAccAWSALBTargetGroupConfig_updateTags(targetGroupName string) string { + return fmt.Sprintf(`resource "aws_alb_target_group" "test" { + name = "%s" + port = 443 + protocol = "HTTPS" + vpc_id = "${aws_vpc.test.id}" + + deregistration_delay = 200 + + stickiness { + type = "lb_cookie" + cookie_duration = 10000 + } + + health_check { + path = "/health" + interval = 60 + port = 8081 + protocol = "HTTP" + timeout = 3 + healthy_threshold = 3 + unhealthy_threshold = 3 + matcher = "200-299" + } + + tags { + Environment = "Production" + Type = "ALB Target Group" + } } resource "aws_vpc" "test" { diff --git a/builtin/providers/aws/resource_aws_alb_test.go b/builtin/providers/aws/resource_aws_alb_test.go index 408733d5d..c70786b99 100644 --- a/builtin/providers/aws/resource_aws_alb_test.go +++ b/builtin/providers/aws/resource_aws_alb_test.go @@ -45,6 +45,37 @@ func TestAccAWSALB_basic(t *testing.T) { }) } +func TestAccAWSALB_tags(t *testing.T) { + var conf elbv2.LoadBalancer + albName := fmt.Sprintf("testaccawsalb-basic-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_alb.alb_test", + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSALBConfig_basic(albName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSALBExists("aws_alb.alb_test", &conf), + resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.%", "1"), + resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.TestName", "TestAccAWSALB_basic"), + ), + }, + { + Config: testAccAWSALBConfig_updatedTags(albName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSALBExists("aws_alb.alb_test", &conf), + resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.%", "2"), + resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.Type", "Sample Type Tag"), + resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.Environment", "Production"), + ), + }, + }, + }) +} + // TestAccAWSALB_noSecurityGroup regression tests the issue in #8264, // where if an ALB is created without a security group, a default one // is assigned. @@ -118,7 +149,7 @@ func TestAccAWSALB_accesslogs(t *testing.T) { resource.TestCheckResourceAttr("aws_alb.alb_test", "subnets.#", "2"), resource.TestCheckResourceAttr("aws_alb.alb_test", "security_groups.#", "1"), resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.%", "1"), - resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.TestName", "TestAccAWSALB_basic"), + resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.TestName", "TestAccAWSALB_basic1"), resource.TestCheckResourceAttr("aws_alb.alb_test", "enable_deletion_protection", "false"), resource.TestCheckResourceAttr("aws_alb.alb_test", "idle_timeout", "50"), resource.TestCheckResourceAttrSet("aws_alb.alb_test", "vpc_id"), @@ -237,6 +268,73 @@ resource "aws_subnet" "alb_test" { } } +resource "aws_security_group" "alb_test" { + name = "allow_all_alb_test" + description = "Used for ALB Testing" + vpc_id = "${aws_vpc.alb_test.id}" + + ingress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags { + TestName = "TestAccAWSALB_basic" + } +}`, albName) +} +func testAccAWSALBConfig_updatedTags(albName string) string { + return fmt.Sprintf(`resource "aws_alb" "alb_test" { + name = "%s" + internal = false + security_groups = ["${aws_security_group.alb_test.id}"] + subnets = ["${aws_subnet.alb_test.*.id}"] + + idle_timeout = 30 + enable_deletion_protection = false + + tags { + Environment = "Production" + Type = "Sample Type Tag" + } +} + +variable "subnets" { + default = ["10.0.1.0/24", "10.0.2.0/24"] + type = "list" +} + +data "aws_availability_zones" "available" {} + +resource "aws_vpc" "alb_test" { + cidr_block = "10.0.0.0/16" + + tags { + TestName = "TestAccAWSALB_basic" + } +} + +resource "aws_subnet" "alb_test" { + count = 2 + vpc_id = "${aws_vpc.alb_test.id}" + cidr_block = "${element(var.subnets, count.index)}" + map_public_ip_on_launch = true + availability_zone = "${element(data.aws_availability_zones.available.names, count.index)}" + + tags { + TestName = "TestAccAWSALB_basic" + } +} + resource "aws_security_group" "alb_test" { name = "allow_all_alb_test" description = "Used for ALB Testing" @@ -278,7 +376,7 @@ func testAccAWSALBConfig_accessLogs(albName, bucketName string) string { } tags { - TestName = "TestAccAWSALB_basic" + TestName = "TestAccAWSALB_basic1" } } diff --git a/builtin/providers/aws/tags.go b/builtin/providers/aws/tags.go index df5d9e2a7..683f85ef7 100644 --- a/builtin/providers/aws/tags.go +++ b/builtin/providers/aws/tags.go @@ -5,6 +5,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/elbv2" "github.com/hashicorp/terraform/helper/schema" ) @@ -17,6 +18,43 @@ func tagsSchema() *schema.Schema { } } +func setElbV2Tags(conn *elbv2.ELBV2, d *schema.ResourceData) error { + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffElbV2Tags(tagsFromMapELBv2(o), tagsFromMapELBv2(n)) + + // Set tags + if len(remove) > 0 { + var tagKeys []*string + for _, tag := range remove { + tagKeys = append(tagKeys, tag.Key) + } + log.Printf("[DEBUG] Removing tags: %#v from %s", remove, d.Id()) + _, err := conn.RemoveTags(&elbv2.RemoveTagsInput{ + ResourceArns: []*string{aws.String(d.Id())}, + TagKeys: tagKeys, + }) + if err != nil { + return err + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %s for %s", create, d.Id()) + _, err := conn.AddTags(&elbv2.AddTagsInput{ + ResourceArns: []*string{aws.String(d.Id())}, + Tags: create, + }) + if err != nil { + return err + } + } + } + + return nil +} + // setTags is a helper to set the tags for a resource. It expects the // tags field to be named "tags" func setTags(conn *ec2.EC2, d *schema.ResourceData) error { @@ -97,3 +135,46 @@ func tagsToMap(ts []*ec2.Tag) map[string]string { return result } + +func diffElbV2Tags(oldTags, newTags []*elbv2.Tag) ([]*elbv2.Tag, []*elbv2.Tag) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + create[*t.Key] = *t.Value + } + + // Build the list of what to remove + var remove []*elbv2.Tag + for _, t := range oldTags { + old, ok := create[*t.Key] + if !ok || old != *t.Value { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapELBv2(create), remove +} + +// tagsToMapELBv2 turns the list of tags into a map. +func tagsToMapELBv2(ts []*elbv2.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + result[*t.Key] = *t.Value + } + + return result +} + +// tagsFromMapELBv2 returns the tags for the given map of data. +func tagsFromMapELBv2(m map[string]interface{}) []*elbv2.Tag { + var result []*elbv2.Tag + for k, v := range m { + result = append(result, &elbv2.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} diff --git a/website/source/docs/providers/aws/r/alb_target_group.html.markdown b/website/source/docs/providers/aws/r/alb_target_group.html.markdown index 9ab94e441..82bf146e8 100644 --- a/website/source/docs/providers/aws/r/alb_target_group.html.markdown +++ b/website/source/docs/providers/aws/r/alb_target_group.html.markdown @@ -38,6 +38,7 @@ The following arguments are supported: * `deregistration_delay` - (Optional) The amount time for Elastic Load Balancing to wait before changing the state of a deregistering target from draining to unused. The range is 0-3600 seconds. The default value is 300 seconds. * `stickiness` - (Optional) A Stickiness block. Stickiness blocks are documented below. * `health_check` - (Optional) A Health Check block. Health Check blocks are documented below. +* `tags` - (Optional) A mapping of tags to assign to the resource. Stickiness Blocks (`stickiness`) support the following: