Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce the aws_ec2_tag resource for managing individual tags on EC2 resources #8457

Merged
merged 35 commits into from
Jun 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
62620f1
Add the initial aws_ec2_tag resource.
Apr 26, 2019
cfbbf57
Add the resource to the provider and write some docs.
Apr 26, 2019
14eb34a
Get code compiling.
Apr 26, 2019
77ddd4e
Get tests working minus the VPC resource causing a dirty plan.
Apr 26, 2019
207b180
Only manage tags if someone has defined them in aws_subnet and aws_vpc.
Apr 26, 2019
ae9e01c
Check error on delete.
Apr 26, 2019
1a08979
do the same for route tables
meekmichael May 6, 2019
7ed006b
Merge branch 'master' into jstump-subnet-tags
Jul 9, 2019
937520c
Use a retry as the EC2 API is eventually consistent.
Jul 9, 2019
107f95b
Merge pull request #1 from meekmichael/meek-rt-tags
Jul 9, 2019
6c1bca4
fixing retry logic - DescribeTags returns no err and an empty list of…
meekmichael Aug 14, 2019
8ccb768
Merge pull request #2 from meekmichael/meek/eventual_tag_consistency
Aug 16, 2019
3f353d5
Merge in master and fix conflict.
Sep 12, 2019
1e82903
Add docs for timeout.
Sep 12, 2019
88ea52e
Handle tag delete in resourceAwsEc2TagRead
Sep 23, 2019
a8b754e
Added [WARN] prefix in the log
Sep 25, 2019
90f2979
Merge pull request #3 from hpamukcu/handle_out_of_band_tag_delete
Sep 30, 2019
72bdc61
Merge branch 'master' into jstump-subnet-tags
davewongillies Nov 20, 2019
776cd2b
Switch to terraform-plugin-sdk
davewongillies Nov 20, 2019
b3f6dc8
add subcategory to website doc
davewongillies Nov 20, 2019
fd5ec59
Prevent VPC and Subnet resources from deleting tags created by aws_ec…
noremmie Jan 16, 2020
dc401c1
Apply eventual consistency mitigation only on create
noremmie Jan 16, 2020
9b181f3
Placate linter
noremmie Jan 16, 2020
6f28df8
Merge pull request #4 from noremmie/nathan/bugfixes
Jan 21, 2020
2a725a5
Merge branch 'master' into jstump-subnet-tags
Feb 25, 2020
8343f86
Merge branch 'master' into jstump-subnet-tags
May 4, 2020
0a60466
fixing broken tests for new tagging mechanism for VPC, subnet, and ro…
iambrianfallon May 4, 2020
c464407
Merge pull request #6 from iambrianfallon/iambrianfallon-subnet-tags
May 14, 2020
755b3b5
Remove configurable timeouts
May 19, 2020
8ce557e
Remove timeout docs
May 19, 2020
aa3a68b
Better docs thanks to bflad
May 19, 2020
0132678
Make retry timeout a static 5 minutes
May 19, 2020
c34afaf
remove conditional tag checks for VPC, subnet, and route table, gener…
iambrianfallon Jun 8, 2020
9b1b82a
make an error message more informative
iambrianfallon Jun 8, 2020
79cee80
add in CheckDestroy for ec2_tag tests to ensure VPCs cleaned up
iambrianfallon Jun 8, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ func Provider() terraform.ResourceProvider {
"aws_ec2_client_vpn_endpoint": resourceAwsEc2ClientVpnEndpoint(),
"aws_ec2_client_vpn_network_association": resourceAwsEc2ClientVpnNetworkAssociation(),
"aws_ec2_fleet": resourceAwsEc2Fleet(),
"aws_ec2_tag": resourceAwsEc2Tag(),
"aws_ec2_traffic_mirror_filter": resourceAwsEc2TrafficMirrorFilter(),
"aws_ec2_traffic_mirror_filter_rule": resourceAwsEc2TrafficMirrorFilterRule(),
"aws_ec2_traffic_mirror_target": resourceAwsEc2TrafficMirrorTarget(),
Expand Down
180 changes: 180 additions & 0 deletions aws/resource_aws_ec2_tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package aws

import (
"fmt"
"log"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func resourceAwsEc2Tag() *schema.Resource {
return &schema.Resource{
Create: resourceAwsEc2TagCreate,
Read: resourceAwsEc2TagRead,
Delete: resourceAwsEc2TagDelete,

Schema: map[string]*schema.Schema{
"resource_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"key": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"value": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}

func extractResourceIDAndKeyFromEc2TagID(id string) (string, string, error) {
parts := strings.Split(id, ":")

if len(parts) != 2 {
return "", "", fmt.Errorf("Invalid resource ID; cannot look up resource: %s", id)
}

return parts[0], parts[1], nil
}

func resourceAwsEc2TagCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn

resourceID := d.Get("resource_id").(string)
key := d.Get("key").(string)
value := d.Get("value").(string)

_, err := conn.CreateTags(&ec2.CreateTagsInput{
Resources: []*string{aws.String(resourceID)},
Tags: []*ec2.Tag{
{
Key: aws.String(key),
Value: aws.String(value),
},
},
})

if err != nil {
return fmt.Errorf("error creating EC2 Tag (%s) for resource (%s): %w", key, resourceID, err)
}

// Handle EC2 eventual consistency on creation
log.Printf("[DEBUG] Waiting for tag %s on resource %s to become available", key, resourceID)
retryError := resource.Retry(5*time.Minute, func() *resource.RetryError {
var tags *ec2.DescribeTagsOutput
tags, err = conn.DescribeTags(&ec2.DescribeTagsInput{
Filters: []*ec2.Filter{
{
Name: aws.String("resource-id"),
Values: []*string{aws.String(resourceID)},
},
{
Name: aws.String("key"),
Values: []*string{aws.String(key)},
},
},
})

if err != nil {
return resource.NonRetryableError(err)
}

// tag not found _yet_
if len(tags.Tags) == 0 {
return resource.RetryableError(&resource.NotFoundError{})
}

return nil
})

if retryError != nil {
if isResourceNotFoundError(err) {
return fmt.Errorf("error creating EC2 Tag (%s) on resource (%s): %w", key, resourceID, err)
}
}

d.SetId(fmt.Sprintf("%s:%s", resourceID, key))
return resourceAwsEc2TagRead(d, meta)
}

func resourceAwsEc2TagRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
id, _, err := extractResourceIDAndKeyFromEc2TagID(d.Id())

if err != nil {
return err
}

key := d.Get("key").(string)
var tags *ec2.DescribeTagsOutput

tags, err = conn.DescribeTags(&ec2.DescribeTagsInput{
Filters: []*ec2.Filter{
{
Name: aws.String("resource-id"),
Values: []*string{aws.String(id)},
},
{
Name: aws.String("key"),
Values: []*string{aws.String(key)},
},
},
})

if err != nil {
return fmt.Errorf("error reading EC2 Tag (%s) on resource (%s): %w", key, id, err)
}

if len(tags.Tags) == 0 {
// The API call did not fail but the tag does not exists on resource
// Did not find the tag, as per contract with TF report:https://www.terraform.io/docs/extend/writing-custom-providers.html
log.Printf("[WARN]There are no tags on resource %s", id)
d.SetId("")
return nil
}

if len(tags.Tags) != 1 {
return fmt.Errorf("Expected exactly 1 tag, got %d tags for key %s", len(tags.Tags), key)
}

tag := tags.Tags[0]
d.Set("value", aws.StringValue(tag.Value))

return nil
}

func resourceAwsEc2TagDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
id, _, err := extractResourceIDAndKeyFromEc2TagID(d.Id())

if err != nil {
return err
}

_, err = conn.DeleteTags(&ec2.DeleteTagsInput{
Resources: []*string{aws.String(id)},
Tags: []*ec2.Tag{
{
Key: aws.String(d.Get("key").(string)),
Value: aws.String(d.Get("value").(string)),
},
},
})

if err != nil {
return fmt.Errorf("error deleting EC2 Tag (%s) on resource (%s): %w", d.Get("key").(string), id, err)
}

return nil
}
106 changes: 106 additions & 0 deletions aws/resource_aws_ec2_tag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package aws

import (
"fmt"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
)

func TestAccAWSEc2ResourceTag_basic(t *testing.T) {
var tag ec2.TagDescription

resource.ParallelTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckVpcDestroy,
Steps: []resource.TestStep{
{
Config: testAccProviderConfigIgnoreTagsKeys1("Name") + testAccEc2ResourceTagConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckEc2ResourceTagExists(
"aws_ec2_tag.test", &tag),
resource.TestCheckResourceAttr("aws_ec2_tag.test", "key", "Name"),
resource.TestCheckResourceAttr("aws_ec2_tag.test", "value", "Hello World"),
),
},
},
})
}

func TestAccAWSEc2ResourceTag_OutOfBandDelete(t *testing.T) {
var tag ec2.TagDescription

resource.ParallelTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckVpcDestroy,
Steps: []resource.TestStep{
{
Config: testAccEc2ResourceTagConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckEc2ResourceTagExists("aws_ec2_tag.test", &tag),
testAccCheckResourceDisappears(testAccProvider, resourceAwsEc2Tag(), "aws_ec2_tag.test"),
),
ExpectNonEmptyPlan: true,
},
},
})
}

func testAccCheckEc2ResourceTagExists(n string, tag *ec2.TagDescription) 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")
}

id, key, err := extractResourceIDAndKeyFromEc2TagID(rs.Primary.ID)
if err != nil {
return fmt.Errorf("Error getting ID or key from EC2 tag ID: %s", rs.Primary.ID)
}
conn := testAccProvider.Meta().(*AWSClient).ec2conn
resp, err := conn.DescribeTags(&ec2.DescribeTagsInput{
Filters: []*ec2.Filter{
{
Name: aws.String("resource-id"),
Values: []*string{aws.String(id)},
},
{
Name: aws.String("key"),
Values: []*string{aws.String(key)},
},
},
})

if err != nil {
return err
}

if len(resp.Tags) == 0 {
return fmt.Errorf("No tags found")
}

*tag = *resp.Tags[0]
// return fmt.Errorf("Tag found %s => %s", aws.StringValue(tag.Key), aws.StringValue(tag.Value))

return nil
}
}

const testAccEc2ResourceTagConfig = `
resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
}

resource "aws_ec2_tag" "test" {
resource_id = "${aws_vpc.test.id}"
key = "Name"
value = "Hello World"
}
`
1 change: 1 addition & 0 deletions aws/resource_aws_route_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ func resourceAwsRouteTableRead(d *schema.ResourceData, meta interface{}) error {
}
d.Set("route", route)

// Tags
if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(rt.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil {
return fmt.Errorf("error setting tags: %s", err)
}
Expand Down
5 changes: 4 additions & 1 deletion website/aws.erb
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,9 @@
<li>
<a href="/docs/providers/aws/r/ec2_fleet.html">aws_ec2_fleet</a>
</li>
<li>
<a href="/docs/providers/aws/r/ec2_tag.html">aws_ec2_tag</a>
</li>
<li>
<a href="/docs/providers/aws/r/ec2_traffic_mirror_filter.html">aws_ec2_traffic_mirror_filter</a>
</li>
Expand Down Expand Up @@ -3610,4 +3613,4 @@
</div>
<% end %>
<%= yield %>
<% end %>
<% end %>
33 changes: 33 additions & 0 deletions website/docs/r/ec2_tag.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
bflad marked this conversation as resolved.
Show resolved Hide resolved
subcategory: "EC2"
layout: "aws"
page_title: "AWS: aws_ec2_tag"
description: |-
Manages a single tag for a given EC2 resource
---

# Resource: aws_ec2_tag

Manages an individual tag for a given EC2 resource. This resource should only be used in cases where EC2 resources are created outside Terraform (e.g. AMIs), being shared via Resource Access Manager (RAM), or implicitly created by other means (e.g. Transit Gateway VPN Attachments).

## Example Usage

```hcl
resource "aws_vpc" "example" {
cidr_block = "10.0.0.0/16"
}

resource "aws_ec2_tag" "example" {
resource_id = "${aws_vpc.example.id}"
key = "Name"
value = "Hello World"
}
```

## Argument Reference

The following arguments are supported:

* `resource_id` - (Required) The ID of the EC2 resource to manage the tag for.
* `key` - (Required) The tag name.
* `value` - (Required) The value of the tag.