Skip to content

Commit

Permalink
resource/aws_ec2_tag: Finish implementation
Browse files Browse the repository at this point in the history
Reference: #8457

Changes:

* Use keyvaluetags package where possible
* Support import and in-place update of value
* Add yellow callout boxes in documentation to disuade usage with parent resources and note lack of ignore_tags usage
* Update examples and testing for real-world implementation of EC2 Transit Gateway VPN Attachment tagging

Output from acceptance testing:

```
--- PASS: TestAccAWSEc2Tag_Value (489.14s)
--- PASS: TestAccAWSEc2Tag_disappears (534.08s)
--- PASS: TestAccAWSEc2Tag_basic (538.95s)
```
  • Loading branch information
bflad committed Jun 13, 2020
1 parent 9a5337a commit 2ca7ee0
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 115 deletions.
130 changes: 50 additions & 80 deletions aws/resource_aws_ec2_tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ 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"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
)

func resourceAwsEc2Tag() *schema.Resource {
return &schema.Resource{
Create: resourceAwsEc2TagCreate,
Read: resourceAwsEc2TagRead,
Update: resourceAwsEc2TagUpdate,
Delete: resourceAwsEc2TagDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"resource_id": {
Expand All @@ -32,14 +35,13 @@ func resourceAwsEc2Tag() *schema.Resource {
"value": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}

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

if len(parts) != 2 {
return "", "", fmt.Errorf("Invalid resource ID; cannot look up resource: %s", id)
Expand All @@ -55,125 +57,93 @@ func resourceAwsEc2TagCreate(d *schema.ResourceData, meta interface{}) error {
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 {
if err := keyvaluetags.Ec2CreateTags(conn, resourceID, map[string]string{key: value}); 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())
resourceID, key, err := extractResourceIDAndKeyFromEc2TagID(d.Id())

if err != nil {
return err
}

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

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

output, err := conn.DescribeTags(input)

if err != nil {
return fmt.Errorf("error reading EC2 Tag (%s) on resource (%s): %w", key, id, err)
return fmt.Errorf("error reading EC2 Tag (%s) for resource (%s): %w", key, resourceID, 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 output == nil {
return fmt.Errorf("error reading EC2 Tag (%s) for resource (%s): empty response", key, resourceID)
}

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

for _, outputTag := range output.Tags {
if aws.StringValue(outputTag.Key) == key {
tag = outputTag
break
}
}

tag := tags.Tags[0]
d.Set("value", aws.StringValue(tag.Value))
if tag == nil {
log.Printf("[WARN] EC2 Tag (%s) for resource (%s) not found, removing from state", key, resourceID)
d.SetId("")
return nil
}

d.Set("key", key)
d.Set("resource_id", resourceID)
d.Set("value", tag.Value)

return nil
}

func resourceAwsEc2TagDelete(d *schema.ResourceData, meta interface{}) error {
func resourceAwsEc2TagUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
id, _, err := extractResourceIDAndKeyFromEc2TagID(d.Id())
resourceID, key, 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 := keyvaluetags.Ec2UpdateTags(conn, resourceID, nil, map[string]string{key: d.Get("value").(string)}); err != nil {
return fmt.Errorf("error updating EC2 Tag (%s) for resource (%s): %w", key, resourceID, err)
}

return resourceAwsEc2TagRead(d, meta)
}

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

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

if err := keyvaluetags.Ec2UpdateTags(conn, resourceID, map[string]string{key: d.Get("value").(string)}, nil); err != nil {
return fmt.Errorf("error deleting EC2 Tag (%s) for resource (%s): %w", key, resourceID, err)
}

return nil
Expand Down
Loading

0 comments on commit 2ca7ee0

Please sign in to comment.