diff --git a/.changelog/20009.txt b/.changelog/20009.txt new file mode 100644 index 00000000000..2b162e60ddd --- /dev/null +++ b/.changelog/20009.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_autoscaling_group_tag +``` diff --git a/aws/provider.go b/aws/provider.go index 90236fbf4ec..b3329801350 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -529,6 +529,7 @@ func Provider() *schema.Provider { "aws_athena_workgroup": resourceAwsAthenaWorkgroup(), "aws_autoscaling_attachment": resourceAwsAutoscalingAttachment(), "aws_autoscaling_group": resourceAwsAutoscalingGroup(), + "aws_autoscaling_group_tag": resourceAwsAutoscalingGroupTag(), "aws_autoscaling_lifecycle_hook": resourceAwsAutoscalingLifecycleHook(), "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), "aws_autoscaling_policy": resourceAwsAutoscalingPolicy(), diff --git a/aws/resource_aws_autoscaling_group_tag.go b/aws/resource_aws_autoscaling_group_tag.go new file mode 100644 index 00000000000..680429b2118 --- /dev/null +++ b/aws/resource_aws_autoscaling_group_tag.go @@ -0,0 +1,143 @@ +package aws + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func resourceAwsAutoscalingGroupTag() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAutoscalingGroupTagCreate, + Read: resourceAwsAutoscalingGroupTagRead, + Update: resourceAwsAutoscalingGroupTagUpdate, + Delete: resourceAwsAutoscalingGroupTagDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "asg_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "tag": { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + "propagate_at_launch": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + }, + } +} + +func extractAutoscalingGroupNameAndKeyFromAutoscalingGroupTagID(id string) (string, string, error) { + parts := strings.SplitN(id, ",", 2) + + if len(parts) != 2 { + return "", "", fmt.Errorf("Invalid resource ID; cannot look up resource: %s", id) + } + + return parts[0], parts[1], nil +} + +func resourceAwsAutoscalingGroupTagCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + + asgName := d.Get("asg_name").(string) + tags := d.Get("tag").([]interface{}) + + tag := tags[0].(map[string]interface{}) + key := tag["key"].(string) + + if err := keyvaluetags.AutoscalingUpdateTags(conn, asgName, autoscalingTagResourceTypeAutoScalingGroup, nil, tags); err != nil { + return fmt.Errorf("error updating Autoscaling Tag (%s) for resource (%s): %w", key, asgName, err) + } + + d.SetId(fmt.Sprintf("%s,%s", asgName, key)) + + return resourceAwsAutoscalingGroupTagRead(d, meta) +} + +func resourceAwsAutoscalingGroupTagRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + asgName, key, err := extractAutoscalingGroupNameAndKeyFromAutoscalingGroupTagID(d.Id()) + + if err != nil { + return err + } + + exists, tagData, err := keyvaluetags.AutoscalingGetTag(conn, asgName, autoscalingTagResourceTypeAutoScalingGroup, key) + + if err != nil { + return fmt.Errorf("error reading Autoscaling Tag (%s) for resource (%s): %w", key, asgName, err) + } + + if !exists { + log.Printf("[WARN] Autoscaling Tag (%s) for resource (%s) not found, removing from state", key, asgName) + d.SetId("") + return nil + } + + d.Set("asg_name", asgName) + + tag := map[string]interface{}{ + "key": key, + "value": tagData.Value, + + "propagate_at_launch": tagData.AdditionalBoolFields["PropagateAtLaunch"], + } + d.Set("tag", []map[string]interface{}{tag}) + + return nil +} + +func resourceAwsAutoscalingGroupTagUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + asgName, key, err := extractAutoscalingGroupNameAndKeyFromAutoscalingGroupTagID(d.Id()) + + if err != nil { + return err + } + + if err := keyvaluetags.AutoscalingUpdateTags(conn, asgName, autoscalingTagResourceTypeAutoScalingGroup, nil, d.Get("tag")); err != nil { + return fmt.Errorf("error updating Autoscaling Tag (%s) for resource (%s): %w", key, asgName, err) + } + + return resourceAwsAutoscalingGroupTagRead(d, meta) +} + +func resourceAwsAutoscalingGroupTagDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + asgName, key, err := extractAutoscalingGroupNameAndKeyFromAutoscalingGroupTagID(d.Id()) + + if err != nil { + return err + } + + if err := keyvaluetags.AutoscalingUpdateTags(conn, asgName, autoscalingTagResourceTypeAutoScalingGroup, d.Get("tag"), nil); err != nil { + return fmt.Errorf("error deleting Autoscaling Tag (%s) for resource (%s): %w", key, asgName, err) + } + + return nil +} diff --git a/aws/resource_aws_autoscaling_group_tag_test.go b/aws/resource_aws_autoscaling_group_tag_test.go new file mode 100644 index 00000000000..b2b899c48c3 --- /dev/null +++ b/aws/resource_aws_autoscaling_group_tag_test.go @@ -0,0 +1,204 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func TestAccAWSAutoscalingGroupTag_basic(t *testing.T) { + resourceName := "aws_autoscaling_group_tag.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAutoscalingGroupTagDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAutoscalingGroupTagConfig("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoscalingGroupTagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tag.0.key", "key1"), + resource.TestCheckResourceAttr(resourceName, "tag.0.value", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSAutoscalingGroupTag_disappears(t *testing.T) { + resourceName := "aws_autoscaling_group_tag.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAutoscalingGroupTagDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAutoscalingGroupTagConfig("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoscalingGroupTagExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAutoscalingGroupTag(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSAutoscalingGroupTag_Value(t *testing.T) { + resourceName := "aws_autoscaling_group_tag.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAutoscalingGroupTagDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAutoscalingGroupTagConfig("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoscalingGroupTagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tag.0.key", "key1"), + resource.TestCheckResourceAttr(resourceName, "tag.0.value", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAutoscalingGroupTagConfig("key1", "value1updated"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoscalingGroupTagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tag.0.key", "key1"), + resource.TestCheckResourceAttr(resourceName, "tag.0.value", "value1updated"), + ), + }, + }, + }) +} + +func testAccCheckAutoscalingGroupTagDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_autoscaling_group_tag" { + continue + } + + asgName, key, err := extractAutoscalingGroupNameAndKeyFromAutoscalingGroupTagID(rs.Primary.ID) + + if err != nil { + return err + } + + exists, _, err := keyvaluetags.AutoscalingGetTag(conn, asgName, autoscalingTagResourceTypeAutoScalingGroup, key) + + if err != nil { + return err + } + + if exists { + return fmt.Errorf("Tag (%s) for resource (%s) still exists", key, asgName) + } + } + + return nil +} + +func testAccCheckAutoscalingGroupTagExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + asgName, key, err := extractAutoscalingGroupNameAndKeyFromAutoscalingGroupTagID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + + exists, _, err := keyvaluetags.AutoscalingGetTag(conn, asgName, autoscalingTagResourceTypeAutoScalingGroup, key) + + if err != nil { + return err + } + + if !exists { + return fmt.Errorf("Tag (%s) for resource (%s) not found", key, asgName) + } + + return nil + } +} + +func testAccAutoscalingGroupTagConfig(key string, value string) string { + return fmt.Sprintf(` +data "aws_ami" "latest_al2" { + owners = ["amazon"] + most_recent = true + + filter { + name = "name" + values = ["amzn2-ami-hvm-*-x86_64-ebs"] + } +} + +resource "aws_launch_template" "test" { + name_prefix = "terraform-test-" + image_id = data.aws_ami.latest_al2.id + instance_type = "t2.nano" +} + +data "aws_availability_zones" "available" { + state = "available" +} + +resource "aws_autoscaling_group" "test" { + lifecycle { + ignore_changes = [tag] + } + + availability_zones = data.aws_availability_zones.available.names + + min_size = 0 + max_size = 0 + + launch_template { + id = aws_launch_template.test.id + version = "$Latest" + } +} + +resource "aws_autoscaling_group_tag" "test" { + asg_name = aws_autoscaling_group.test.name + + tag { + key = %[1]q + value = %[2]q + + propagate_at_launch = true + } +} +`, key, value) +} diff --git a/website/docs/r/autoscaling_group_tag.html.markdown b/website/docs/r/autoscaling_group_tag.html.markdown new file mode 100644 index 00000000000..327f3b51a2b --- /dev/null +++ b/website/docs/r/autoscaling_group_tag.html.markdown @@ -0,0 +1,70 @@ +--- +subcategory: "Autoscaling" +layout: "aws" +page_title: "AWS: aws_autoscaling_group_tag" +description: |- + Manages an individual Autoscaling Group tag +--- + +# Resource: aws_autoscaling_group_tag + +Manages an individual Autoscaling Group (ASG) tag. This resource should only be used in cases where ASGs are created outside Terraform (e.g. ASGs implicitly created by EKS Node Groups). + +~> **NOTE:** This tagging resource should not be combined with the Terraform resource for managing the parent resource. For example, using `aws_autoscaling_group` and `aws_autoscaling_group_tag` to manage tags of the same ASG will cause a perpetual difference where the `aws_autoscaling_group` resource will try to remove the tag being added by the `aws_autoscaling_group_tag` resource. + +~> **NOTE:** This tagging resource does not use the [provider `ignore_tags` configuration](/docs/providers/aws/index.html#ignore_tags). + +## Example Usage + +```terraform +resource "aws_eks_node_group" "example" { + cluster_name = "example" + node_group_name = "example" + + # ... other configuration ... +} + +resource "aws_autoscaling_group_tag" "example" { + for_each = toset( + [for asg in flatten( + [for resources in aws_eks_node_group.example.resources: resources.autoscaling_groups] + ): asg.name] + ) + + asg_name = each.value + + tag { + key = "k8s.io/cluster-autoscaler/node-template/label/eks.amazonaws.com/capacityType" + value = "SPOT" + + propagate_at_launch = false + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `asg_name` - (Required) The name of the Autoscaling Group to apply the tag to. +* `tag` - (Required) The tag to create. The `tag` block is documented below. + +The `tag` block supports the following arguments: + +* `key` - (Required) Tag name. +* `value` - (Required) Tag value. +* `propagate_at_launch` - (Required) Whether to propagate the tags to instances launched by the ASG. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - ASG name and key, separated by a comma (`,`) + +## Import + +`aws_autoscaling_group_tag` can be imported by using the ASG name and key, separated by a comma (`,`), e.g. + +``` +$ terraform import aws_autoscaling_group.example asg-,k8s.io/cluster-autoscaler/node-template/label/eks.amazonaws.com/capacityType +```