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

Feature/add support resource tags for aws cloudwatch event rule resource #8076

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 14 additions & 1 deletion aws/resource_aws_cloudwatch_event_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func resourceAwsCloudWatchEventRule() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"tags": tagsSchema(),
},
}
}
Expand Down Expand Up @@ -121,6 +122,10 @@ func resourceAwsCloudWatchEventRuleCreate(d *schema.ResourceData, meta interface

log.Printf("[INFO] CloudWatch Event Rule %q created", *out.RuleArn)

if err := setTagsCloudWatchEvents(conn, d, aws.StringValue(out.RuleArn)); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future enhancement, it appears PutRule supports a Tags parameter directly: https://docs.aws.amazon.com/sdk-for-go/api/service/cloudwatchevents/#PutRuleInput

Instead of setting the Tags via TagResource we may be able to simplify with something like:

Tags: tagsFromMapCloudWatchEvents(d.Get("tags").(map[string]interface{})),

Which (at least on creation) may be a requirement in some environments with IAM-based restrictions based on tags.

The current implementation is passing acceptance testing though, so for now we can get this in as-is.

return fmt.Errorf("Error creating tags for %s: %s", d.Id(), err)
}

return resourceAwsCloudWatchEventRuleUpdate(d, meta)
}

Expand Down Expand Up @@ -163,7 +168,9 @@ func resourceAwsCloudWatchEventRuleRead(d *schema.ResourceData, meta interface{}
}
log.Printf("[DEBUG] Setting boolean state: %t", boolState)
d.Set("is_enabled", boolState)

if err := saveTagsCloudWatchEvents(conn, d, aws.StringValue(out.Arn)); err != nil {
return fmt.Errorf("error setting tags: %s", err)
}
return nil
}

Expand Down Expand Up @@ -217,6 +224,12 @@ func resourceAwsCloudWatchEventRuleUpdate(d *schema.ResourceData, meta interface
log.Printf("[DEBUG] CloudWatch Event Rule (%q) disabled", d.Id())
}

if d.HasChange("tags") {
if err := setTagsCloudWatchEvents(conn, d, d.Get("arn").(string)); err != nil {
return fmt.Errorf("Error updating tags for %s: %s", d.Id(), err)
}
}

return resourceAwsCloudWatchEventRuleRead(d, meta)
}

Expand Down
86 changes: 86 additions & 0 deletions aws/resource_aws_cloudwatch_event_rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,51 @@ func TestAccAWSCloudWatchEventRule_prefix(t *testing.T) {
})
}

func TestAccAWSCloudWatchEventRule_tags(t *testing.T) {
var rule events.DescribeRuleOutput
resourceName := "aws_cloudwatch_event_rule.default"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSCloudWatchEventRuleConfig_tags,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudWatchEventRuleExists(resourceName, &rule),
resource.TestCheckResourceAttr(resourceName, "tags.%", "2"),
resource.TestCheckResourceAttr(resourceName, "tags.fizz", "buzz"),
resource.TestCheckResourceAttr(resourceName, "tags.foo", "bar"),
),
},
{
Config: testAccAWSCloudWatchEventRuleConfig_updateTags,
bflad marked this conversation as resolved.
Show resolved Hide resolved
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudWatchEventRuleExists(resourceName, &rule),
resource.TestCheckResourceAttr(resourceName, "tags.%", "3"),
resource.TestCheckResourceAttr(resourceName, "tags.fizz", "buzz"),
resource.TestCheckResourceAttr(resourceName, "tags.foo", "bar2"),
resource.TestCheckResourceAttr(resourceName, "tags.good", "bad"),
),
},
{
Config: testAccAWSCloudWatchEventRuleConfig_removeTags,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudWatchEventRuleExists(resourceName, &rule),
resource.TestCheckResourceAttr(resourceName, "tags.%", "1"),
resource.TestCheckResourceAttr(resourceName, "tags.fizz", "buzz"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func TestAccAWSCloudWatchEventRule_full(t *testing.T) {
var rule events.DescribeRuleOutput

Expand All @@ -154,6 +199,8 @@ func TestAccAWSCloudWatchEventRule_full(t *testing.T) {
resource.TestCheckResourceAttr("aws_cloudwatch_event_rule.moobar", "description", "He's not dead, he's just resting!"),
resource.TestCheckResourceAttr("aws_cloudwatch_event_rule.moobar", "role_arn", ""),
testAccCheckCloudWatchEventRuleEnabled("aws_cloudwatch_event_rule.moobar", "DISABLED", &rule),
resource.TestCheckResourceAttr("aws_cloudwatch_event_rule.moobar", "tags.%", "1"),
resource.TestCheckResourceAttr("aws_cloudwatch_event_rule.moobar", "tags.Name", "tf-acc-cw-event-rule-full"),
),
},
},
Expand Down Expand Up @@ -356,6 +403,42 @@ PATTERN
}
`

var testAccAWSCloudWatchEventRuleConfig_tags = `
resource "aws_cloudwatch_event_rule" "default" {
name = "tf-acc-cw-event-rule-tags"
schedule_expression = "rate(1 hour)"

tags = {
fizz = "buzz"
foo = "bar"
}
}
`

var testAccAWSCloudWatchEventRuleConfig_updateTags = `
resource "aws_cloudwatch_event_rule" "default" {
name = "tf-acc-cw-event-rule-tags"
schedule_expression = "rate(1 hour)"

tags = {
fizz = "buzz"
foo = "bar2"
good = "bad"
}
}
`

var testAccAWSCloudWatchEventRuleConfig_removeTags = `
resource "aws_cloudwatch_event_rule" "default" {
name = "tf-acc-cw-event-rule-tags"
schedule_expression = "rate(1 hour)"

tags = {
fizz = "buzz"
}
}
`

var testAccAWSCloudWatchEventRuleConfig_full = `
resource "aws_cloudwatch_event_rule" "moobar" {
name = "tf-acc-cw-event-rule-full"
Expand All @@ -365,6 +448,9 @@ resource "aws_cloudwatch_event_rule" "moobar" {
PATTERN
description = "He's not dead, he's just resting!"
is_enabled = false
tags = {
Name = "tf-acc-cw-event-rule-full"
}
}
`

Expand Down
133 changes: 133 additions & 0 deletions aws/tagsCloudWatchEvent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package aws

import (
"fmt"
"log"
"regexp"

"github.com/aws/aws-sdk-go/aws"
events "github.com/aws/aws-sdk-go/service/cloudwatchevents"
"github.com/hashicorp/terraform/helper/schema"
)

// setTags is a helper to set the tags for a resource. It expects the
// tags field to be named "tags"
func setTagsCloudWatchEvents(conn *events.CloudWatchEvents, d *schema.ResourceData, arn string) error {
if d.HasChange("tags") {
oraw, nraw := d.GetChange("tags")
o := oraw.(map[string]interface{})
n := nraw.(map[string]interface{})
create, remove := diffTagsCloudWatchEvents(tagsFromMapCloudWatchEvents(o), tagsFromMapCloudWatchEvents(n))

// Set tags
if len(remove) > 0 {
log.Printf("[DEBUG] Removing tags: %#v", remove)
k := make([]*string, 0, len(remove))
for _, t := range remove {
k = append(k, t.Key)
}
_, err := conn.UntagResource(&events.UntagResourceInput{
ResourceARN: aws.String(arn),
TagKeys: k,
})
if err != nil {
return err
}
}
if len(create) > 0 {
log.Printf("[DEBUG] Creating tags: %#v", create)
_, err := conn.TagResource(&events.TagResourceInput{
ResourceARN: aws.String(arn),
Tags: create,
})
if 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 diffTagsCloudWatchEvents(oldTags, newTags []*events.Tag) ([]*events.Tag, []*events.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 []*events.Tag
for _, t := range oldTags {
old, ok := create[*t.Key]
if !ok || old != *t.Value {
// Delete it!
remove = append(remove, t)
}
}

return tagsFromMapCloudWatchEvents(create), remove
}

// tagsFromMap returns the tags for the given map of data.
func tagsFromMapCloudWatchEvents(m map[string]interface{}) []*events.Tag {
var result []*events.Tag
for k, v := range m {
t := &events.Tag{
Key: aws.String(k),
Value: aws.String(v.(string)),
}
if !tagIgnoredCloudWatchEvents(t) {
result = append(result, t)
}
}

return result
}

// tagsToMap turns the list of tags into a map.
func tagsToMapCloudWatchEvents(ts []*events.Tag) map[string]string {
result := make(map[string]string)
for _, t := range ts {
if !tagIgnoredCloudWatchEvents(t) {
result[*t.Key] = *t.Value
}
}

return result
}

func saveTagsCloudWatchEvents(conn *events.CloudWatchEvents, d *schema.ResourceData, arn string) error {
resp, err := conn.ListTagsForResource(&events.ListTagsForResourceInput{
ResourceARN: aws.String(arn),
})

if err != nil {
return fmt.Errorf("Error retreiving tags for ARN: %s", arn)
}

var tagList []*events.Tag
if len(resp.Tags) > 0 {
tagList = resp.Tags
}

return d.Set("tags", tagsToMapCloudWatchEvents(tagList))
}

// compare a tag against a list of strings and checks if it should
// be ignored or not
func tagIgnoredCloudWatchEvents(t *events.Tag) bool {
filter := []string{"^aws:"}
for _, v := range filter {
log.Printf("[DEBUG] Matching %v with %v\n", v, *t.Key)
r, _ := regexp.MatchString(v, *t.Key)
if r {
log.Printf("[DEBUG] Found AWS specific tag %s (val: %s), ignoring.\n", *t.Key, *t.Value)
return true
}
}
return false
}
77 changes: 77 additions & 0 deletions aws/tagsCloudWatchEvent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package aws

import (
"reflect"
"testing"

"github.com/aws/aws-sdk-go/aws"
events "github.com/aws/aws-sdk-go/service/cloudwatchevents"
)

func TestDiffCloudWatchEventTags(t *testing.T) {
cases := []struct {
Old, New map[string]interface{}
Create, Remove map[string]string
}{
// Basic add/remove
{
Old: map[string]interface{}{
"foo": "bar",
},
New: map[string]interface{}{
"bar": "baz",
},
Create: map[string]string{
"bar": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
},

// Modify
{
Old: map[string]interface{}{
"foo": "bar",
},
New: map[string]interface{}{
"foo": "baz",
},
Create: map[string]string{
"foo": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
},
}

for i, tc := range cases {
c, r := diffTagsCloudWatchEvents(tagsFromMapCloudWatchEvents(tc.Old), tagsFromMapCloudWatchEvents(tc.New))
cm := tagsToMapCloudWatchEvents(c)
rm := tagsToMapCloudWatchEvents(r)
if !reflect.DeepEqual(cm, tc.Create) {
t.Fatalf("%d: bad create: %#v", i, cm)
}
if !reflect.DeepEqual(rm, tc.Remove) {
t.Fatalf("%d: bad remove: %#v", i, rm)
}
}
}

func TestIgnoringTagsCloudWatchEvents(t *testing.T) {
var ignoredTags []*events.Tag
ignoredTags = append(ignoredTags, &events.Tag{
Key: aws.String("aws:cloudformation:logical-id"),
Value: aws.String("foo"),
})
ignoredTags = append(ignoredTags, &events.Tag{
Key: aws.String("aws:foo:bar"),
Value: aws.String("baz"),
})
for _, tag := range ignoredTags {
if !tagIgnoredCloudWatchEvents(tag) {
t.Fatalf("Tag %v with value %v not ignored, but should be!", *tag.Key, *tag.Value)
}
}
}
1 change: 1 addition & 0 deletions website/docs/r/cloudwatch_event_rule.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ The following arguments are supported:
* `description` - (Optional) The description of the rule.
* `role_arn` - (Optional) The Amazon Resource Name (ARN) associated with the role that is used for target invocation.
* `is_enabled` - (Optional) Whether the rule should be enabled (defaults to `true`).
* `tags` - (Optional) A mapping of tags to assign to the resource.

## Attributes Reference

Expand Down