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

Add support for resource tags aws cloudwatch metric alarm #8168

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
12 changes: 12 additions & 0 deletions aws/resource_aws_cloudwatch_metric_alarm.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ func resourceAwsCloudWatchMetricAlarm() *schema.Resource {
Computed: true,
ValidateFunc: validation.StringInSlice([]string{"evaluate", "ignore"}, true),
},

"tags": tagsSchema(),
},
}
}
Expand Down Expand Up @@ -310,6 +312,10 @@ func resourceAwsCloudWatchMetricAlarmRead(d *schema.ResourceData, meta interface
d.Set("treat_missing_data", a.TreatMissingData)
d.Set("evaluate_low_sample_count_percentiles", a.EvaluateLowSampleCountPercentile)

if err := saveTagsCloudWatch(meta.(*AWSClient).cloudwatchconn, d, aws.StringValue(a.AlarmArn)); err != nil {
return fmt.Errorf("error setting tags: %s", err)
}

return nil
}

Expand All @@ -324,6 +330,11 @@ func resourceAwsCloudWatchMetricAlarmUpdate(d *schema.ResourceData, meta interfa
}
log.Println("[INFO] CloudWatch Metric Alarm updated")

// Tags are cannot update by PutMetricAlarm.
if err := setTagsCloudWatch(conn, d, d.Get("arn").(string)); err != nil {
return fmt.Errorf("error updating tags for %s: %s", d.Id(), err)
}

return resourceAwsCloudWatchMetricAlarmRead(d, meta)
}

Expand Down Expand Up @@ -359,6 +370,7 @@ func getAwsCloudWatchPutMetricAlarmInput(d *schema.ResourceData) cloudwatch.PutM
EvaluationPeriods: aws.Int64(int64(d.Get("evaluation_periods").(int))),
Threshold: aws.Float64(d.Get("threshold").(float64)),
TreatMissingData: aws.String(d.Get("treat_missing_data").(string)),
Tags: tagsFromMapCloudWatch(d.Get("tags").(map[string]interface{})),
}

if v := d.Get("actions_enabled"); v != nil {
Expand Down
121 changes: 121 additions & 0 deletions aws/resource_aws_cloudwatch_metric_alarm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,55 @@ func TestAccAWSCloudWatchMetricAlarm_missingStatistic(t *testing.T) {
})
}

func TestAccAWSCloudWatchMetricAlarm_tags(t *testing.T) {
var alarm cloudwatch.MetricAlarm
resourceName := "aws_cloudwatch_metric_alarm.foobar"
rInt := acctest.RandInt()

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSCloudWatchMetricAlarmDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSCloudWatchMetricAlarmConfigTags(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudWatchMetricAlarmExists(resourceName, &alarm),
resource.TestCheckResourceAttr(resourceName, "tags.%", "3"),
resource.TestCheckResourceAttr(resourceName, "tags.Name", fmt.Sprintf("terraform-test-foobar%d", rInt)),
resource.TestCheckResourceAttr(resourceName, "tags.fizz", "buzz"),
resource.TestCheckResourceAttr(resourceName, "tags.foo", "bar"),
),
},
{
Config: testAccAWSCloudWatchMetricAlarmConfigUpdateTags(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudWatchMetricAlarmExists(resourceName, &alarm),
resource.TestCheckResourceAttr(resourceName, "tags.%", "4"),
resource.TestCheckResourceAttr(resourceName, "tags.Name", fmt.Sprintf("terraform-test-foobar%d", rInt)),
resource.TestCheckResourceAttr(resourceName, "tags.fizz", "buzz"),
resource.TestCheckResourceAttr(resourceName, "tags.foo", "bar2"),
resource.TestCheckResourceAttr(resourceName, "tags.good", "bad"),
),
},
{
Config: testAccAWSCloudWatchMetricAlarmConfigRemoveTags(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudWatchMetricAlarmExists(resourceName, &alarm),
resource.TestCheckResourceAttr(resourceName, "tags.%", "2"),
resource.TestCheckResourceAttr(resourceName, "tags.Name", fmt.Sprintf("terraform-test-foobar%d", rInt)),
resource.TestCheckResourceAttr(resourceName, "tags.fizz", "buzz"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func testAccCheckCloudWatchMetricAlarmDimension(n, k, v string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
Expand Down Expand Up @@ -771,3 +820,75 @@ resource "aws_cloudwatch_metric_alarm" "test" {
}
`, rName)
}

func testAccAWSCloudWatchMetricAlarmConfigTags(rInt int) string {
return fmt.Sprintf(`
resource "aws_cloudwatch_metric_alarm" "foobar" {
alarm_name = "terraform-test-foobar%[1]d"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "120"
statistic = "Average"
threshold = "80"
alarm_description = "This metric monitors ec2 cpu utilization"
insufficient_data_actions = []
dimensions = {
InstanceId = "i-abc123"
}
tags = {
Name = "terraform-test-foobar%[1]d"
fizz = "buzz"
foo = "bar"
}
}`, rInt)
}

func testAccAWSCloudWatchMetricAlarmConfigUpdateTags(rInt int) string {
return fmt.Sprintf(`
resource "aws_cloudwatch_metric_alarm" "foobar" {
alarm_name = "terraform-test-foobar%[1]d"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "120"
statistic = "Average"
threshold = "80"
alarm_description = "This metric monitors ec2 cpu utilization"
insufficient_data_actions = []
dimensions = {
InstanceId = "i-abc123"
}
tags = {
Name = "terraform-test-foobar%[1]d"
fizz = "buzz"
foo = "bar2"
good = "bad"
}
}`, rInt)
}

func testAccAWSCloudWatchMetricAlarmConfigRemoveTags(rInt int) string {
return fmt.Sprintf(`
resource "aws_cloudwatch_metric_alarm" "foobar" {
alarm_name = "terraform-test-foobar%[1]d"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "120"
statistic = "Average"
threshold = "80"
alarm_description = "This metric monitors ec2 cpu utilization"
insufficient_data_actions = []
dimensions = {
InstanceId = "i-abc123"
}
tags = {
Name = "terraform-test-foobar%[1]d"
fizz = "buzz"
}
}`, rInt)
}
133 changes: 133 additions & 0 deletions aws/tagsCloudWatch.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"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"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 setTagsCloudWatch(conn *cloudwatch.CloudWatch, 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 := diffTagsCloudWatch(tagsFromMapCloudWatch(o), tagsFromMapCloudWatch(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(&cloudwatch.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(&cloudwatch.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 diffTagsCloudWatch(oldTags, newTags []*cloudwatch.Tag) ([]*cloudwatch.Tag, []*cloudwatch.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 []*cloudwatch.Tag
for _, t := range oldTags {
old, ok := create[*t.Key]
if !ok || old != *t.Value {
// Delete it!
remove = append(remove, t)
}
}

return tagsFromMapCloudWatch(create), remove
}

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

return result
}

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

return result
}

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

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

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

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

// compare a tag against a list of strings and checks if it should
// be ignored or not
func tagIgnoredCloudWatch(t *cloudwatch.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/tagsCloudWatch_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"
"github.com/aws/aws-sdk-go/service/cloudwatch"
)

func TestDiffCloudWatchTags(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 := diffTagsCloudWatch(tagsFromMapCloudWatch(tc.Old), tagsFromMapCloudWatch(tc.New))
cm := tagsToMapCloudWatch(c)
rm := tagsToMapCloudWatch(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 TestIgnoringTagsCloudWatch(t *testing.T) {
var ignoredTags []*cloudwatch.Tag
ignoredTags = append(ignoredTags, &cloudwatch.Tag{
Key: aws.String("aws:cloudformation:logical-id"),
Value: aws.String("foo"),
})
ignoredTags = append(ignoredTags, &cloudwatch.Tag{
Key: aws.String("aws:foo:bar"),
Value: aws.String("baz"),
})
for _, tag := range ignoredTags {
if !tagIgnoredCloudWatch(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_metric_alarm.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ If you specify `evaluate` or omit this parameter, the alarm will always be
evaluated and possibly change state no matter how many data points are available.
The following values are supported: `ignore`, and `evaluate`.
* `metric_query` (Optional) Enables you to create an alarm based on a metric math expression. You may specify at most 20.
* `tags` - (Optional) A mapping of tags to assign to the resource.

~> **NOTE:** If you specify at least one `metric_query`, you may not specify a `metric_name`, `namespace`, `period` or `statistic`. If you do not specify a `metric_query`, you must specify each of these (although you may use `extended_statistic` instead of `statistic`).

Expand Down