From a987d7778b1451420b6ed09d9d89c96ab80ee600 Mon Sep 17 00:00:00 2001 From: Roberth Kulbin Date: Wed, 2 Sep 2020 19:08:01 +0100 Subject: [PATCH] r/aws_cloudwatch_composite_alarm: add resource --- aws/provider.go | 1 + ...resource_aws_cloudwatch_composite_alarm.go | 269 ++++++++++++++++++ ...rce_aws_cloudwatch_composite_alarm_test.go | 256 +++++++++++++++++ .../cloudwatch_composite_alarm.html.markdown | 56 ++++ 4 files changed, 582 insertions(+) create mode 100644 aws/resource_aws_cloudwatch_composite_alarm.go create mode 100644 aws/resource_aws_cloudwatch_composite_alarm_test.go create mode 100644 website/docs/r/cloudwatch_composite_alarm.html.markdown diff --git a/aws/provider.go b/aws/provider.go index 0543109d0f13..d98ac926dc75 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -513,6 +513,7 @@ func Provider() *schema.Provider { "aws_cloudhsm_v2_cluster": resourceAwsCloudHsmV2Cluster(), "aws_cloudhsm_v2_hsm": resourceAwsCloudHsmV2Hsm(), "aws_cognito_resource_server": resourceAwsCognitoResourceServer(), + "aws_cloudwatch_composite_alarm": resourceAwsCloudWatchCompositeAlarm(), "aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(), "aws_cloudwatch_dashboard": resourceAwsCloudWatchDashboard(), "aws_codedeploy_app": resourceAwsCodeDeployApp(), diff --git a/aws/resource_aws_cloudwatch_composite_alarm.go b/aws/resource_aws_cloudwatch_composite_alarm.go new file mode 100644 index 000000000000..dada6aa8f425 --- /dev/null +++ b/aws/resource_aws_cloudwatch_composite_alarm.go @@ -0,0 +1,269 @@ +package aws + +import ( + "context" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func resourceAwsCloudWatchCompositeAlarm() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAwsCloudWatchCompositeAlarmCreate, + ReadContext: resourceAwsCloudWatchCompositeAlarmRead, + UpdateContext: resourceAwsCloudWatchCompositeAlarmUpdate, + DeleteContext: resourceAwsCloudWatchCompositeAlarmDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "actions_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "alarm_actions": { + Type: schema.TypeSet, + Optional: true, + Set: schema.HashString, + MaxItems: 5, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateArn, + }, + }, + "alarm_description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 1024), + }, + "alarm_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 255), + }, + "alarm_rule": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 10240), + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "insufficient_data_actions": { + Type: schema.TypeSet, + Optional: true, + Set: schema.HashString, + MaxItems: 5, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateArn, + }, + }, + "ok_actions": { + Type: schema.TypeSet, + Optional: true, + Set: schema.HashString, + MaxItems: 5, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateArn, + }, + }, + "tags": tagsSchema(), + }, + } +} + +func resourceAwsCloudWatchCompositeAlarmCreate( + ctx context.Context, + d *schema.ResourceData, + meta interface{}, +) diag.Diagnostics { + conn := meta.(*AWSClient).cloudwatchconn + name := d.Get("alarm_name").(string) + + input := expandAwsCloudWatchPutCompositeAlarmInput(d) + _, err := conn.PutCompositeAlarmWithContext(ctx, &input) + if err != nil { + return diag.Errorf("create composite alarm: %s", err) + } + + log.Printf("[INFO] Created Composite Alarm %s.", name) + d.SetId(name) + + return resourceAwsCloudWatchCompositeAlarmRead(ctx, d, meta) +} + +func resourceAwsCloudWatchCompositeAlarmRead( + ctx context.Context, + d *schema.ResourceData, + meta interface{}, +) diag.Diagnostics { + conn := meta.(*AWSClient).cloudwatchconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + name := d.Id() + + alarm, ok, err := getAwsCloudWatchCompositeAlarm(ctx, conn, name) + switch { + case err != nil: + return diag.FromErr(err) + case !ok: + log.Printf("[WARN] Composite alarm %s has disappeared!", name) + d.SetId("") + return nil + } + + d.Set("actions_enabled", alarm.ActionsEnabled) + + if err := d.Set("alarm_actions", flattenStringSet(alarm.AlarmActions)); err != nil { + return diag.Errorf("set alarm_actions: %s", err) + } + + d.Set("alarm_description", alarm.AlarmDescription) + d.Set("alarm_name", alarm.AlarmName) + d.Set("alarm_rule", alarm.AlarmRule) + d.Set("arn", alarm.AlarmArn) + + if err := d.Set("insufficient_data_actions", flattenStringSet(alarm.InsufficientDataActions)); err != nil { + return diag.Errorf("set insufficient_data_actions: %s", err) + } + + if err := d.Set("ok_actions", flattenStringSet(alarm.OKActions)); err != nil { + return diag.Errorf("set ok_actions: %s", err) + } + + tags, err := keyvaluetags.CloudwatchListTags(conn, aws.StringValue(alarm.AlarmArn)) + if err != nil { + return diag.Errorf("list tags of alarm: %s", err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return diag.Errorf("set tags: %s", err) + } + + return nil +} + +func resourceAwsCloudWatchCompositeAlarmUpdate( + ctx context.Context, + d *schema.ResourceData, + meta interface{}, +) diag.Diagnostics { + conn := meta.(*AWSClient).cloudwatchconn + name := d.Id() + + log.Printf("[INFO] Updating Composite Alarm %s...", name) + + input := expandAwsCloudWatchPutCompositeAlarmInput(d) + _, err := conn.PutCompositeAlarmWithContext(ctx, &input) + if err != nil { + return diag.Errorf("create composite alarm: %s", err) + } + + arn := d.Get("arn").(string) + if d.HasChange("tags") { + o, n := d.GetChange("tags") + + if err := keyvaluetags.CloudwatchUpdateTags(conn, arn, o, n); err != nil { + return diag.Errorf("update tags: %s", err) + } + } + + return resourceAwsCloudWatchCompositeAlarmRead(ctx, d, meta) +} + +func resourceAwsCloudWatchCompositeAlarmDelete( + ctx context.Context, + d *schema.ResourceData, + meta interface{}, +) diag.Diagnostics { + conn := meta.(*AWSClient).cloudwatchconn + name := d.Id() + + log.Printf("[INFO] Deleting Composite Alarm %s...", name) + + input := cloudwatch.DeleteAlarmsInput{ + AlarmNames: aws.StringSlice([]string{name}), + } + + _, err := conn.DeleteAlarmsWithContext(ctx, &input) + switch { + case isAWSErr(err, "ResourceNotFound", ""): + log.Printf("[WARN] Composite Alarm %s has disappeared!", name) + return nil + case err != nil: + return diag.FromErr(err) + } + + return nil +} + +func expandAwsCloudWatchPutCompositeAlarmInput(d *schema.ResourceData) cloudwatch.PutCompositeAlarmInput { + out := cloudwatch.PutCompositeAlarmInput{} + + if v, ok := d.GetOk("actions_enabled"); ok { + out.ActionsEnabled = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("alarm_actions"); ok { + out.AlarmActions = expandStringSet(v.(*schema.Set)) + } + + if v, ok := d.GetOk("alarm_description"); ok { + out.AlarmDescription = aws.String(v.(string)) + } + + if v, ok := d.GetOk("alarm_name"); ok { + out.AlarmName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("alarm_rule"); ok { + out.AlarmRule = aws.String(v.(string)) + } + + if v, ok := d.GetOk("insufficient_data_actions"); ok { + out.InsufficientDataActions = expandStringSet(v.(*schema.Set)) + } + + if v, ok := d.GetOk("ok_actions"); ok { + out.OKActions = expandStringSet(v.(*schema.Set)) + } + + if v, ok := d.GetOk("tags"); ok { + out.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().CloudwatchTags() + } + + return out +} + +func getAwsCloudWatchCompositeAlarm( + ctx context.Context, + conn *cloudwatch.CloudWatch, + name string, +) (*cloudwatch.CompositeAlarm, bool, error) { + input := cloudwatch.DescribeAlarmsInput{ + AlarmNames: aws.StringSlice([]string{name}), + AlarmTypes: aws.StringSlice([]string{cloudwatch.AlarmTypeCompositeAlarm}), + } + + output, err := conn.DescribeAlarmsWithContext(ctx, &input) + switch { + case err != nil: + return nil, false, err + case len(output.CompositeAlarms) != 1: + return nil, false, nil + } + + return output.CompositeAlarms[0], true, nil +} diff --git a/aws/resource_aws_cloudwatch_composite_alarm_test.go b/aws/resource_aws_cloudwatch_composite_alarm_test.go new file mode 100644 index 000000000000..fb9862b5c8bb --- /dev/null +++ b/aws/resource_aws_cloudwatch_composite_alarm_test.go @@ -0,0 +1,256 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func testAccCheckAwsCloudWatchCompositeAlarmExists(n string, alarm *cloudwatch.CompositeAlarm) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).cloudwatchconn + params := cloudwatch.DescribeAlarmsInput{ + AlarmNames: []*string{aws.String(rs.Primary.ID)}, + AlarmTypes: []*string{aws.String(cloudwatch.AlarmTypeCompositeAlarm)}, + } + resp, err := conn.DescribeAlarms(¶ms) + if err != nil { + return err + } + if len(resp.CompositeAlarms) == 0 { + return fmt.Errorf("Alarm not found") + } + *alarm = *resp.CompositeAlarms[0] + + return nil + } +} + +func testAccCheckAwsCloudWatchCompositeAlarmDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudwatchconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudwatch_composite_alarm" { + continue + } + + params := cloudwatch.DescribeAlarmsInput{ + AlarmNames: []*string{aws.String(rs.Primary.ID)}, + } + + resp, err := conn.DescribeAlarms(¶ms) + + if err == nil { + if len(resp.MetricAlarms) != 0 && + *resp.MetricAlarms[0].AlarmName == rs.Primary.ID { + return fmt.Errorf("Alarm Still Exists: %s", rs.Primary.ID) + } + } + } + + return nil +} + +func TestAccAwsCloudWatchCompositeAlarm_basic(t *testing.T) { + alarm := cloudwatch.CompositeAlarm{} + suffix := acctest.RandString(8) + resourceName := "aws_cloudwatch_composite_alarm.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsCloudWatchCompositeAlarmDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_create(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName, &alarm), + resource.TestCheckResourceAttr(resourceName, "alarm_actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "alarm_description", "Test 1"), + resource.TestCheckResourceAttr(resourceName, "alarm_name", "tf-test-composite-"+suffix), + resource.TestCheckResourceAttr(resourceName, "alarm_rule", fmt.Sprintf("ALARM(tf-test-metric-0-%[1]s) OR ALARM(tf-test-metric-1-%[1]s)", suffix)), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "cloudwatch", regexp.MustCompile(`alarm:.+`)), + resource.TestCheckResourceAttr(resourceName, "insufficient_data_actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "ok_actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_update(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName, &alarm), + resource.TestCheckResourceAttr(resourceName, "alarm_actions.#", "2"), + resource.TestCheckResourceAttr(resourceName, "alarm_description", "Test 2"), + resource.TestCheckResourceAttr(resourceName, "alarm_name", "tf-test-composite-"+suffix), + resource.TestCheckResourceAttr(resourceName, "alarm_rule", fmt.Sprintf("ALARM(tf-test-metric-0-%[1]s)", suffix)), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "cloudwatch", regexp.MustCompile(`alarm:.+`)), + resource.TestCheckResourceAttr(resourceName, "insufficient_data_actions.#", "2"), + resource.TestCheckResourceAttr(resourceName, "ok_actions.#", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + ), + }, + }, + }) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_create(suffix string) string { + return fmt.Sprintf(` +resource "aws_cloudwatch_metric_alarm" "test" { + count = 2 + + alarm_name = "tf-test-metric-${count.index}-%[1]s" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = 2 + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = 120 + statistic = "Average" + threshold = 80 + + dimensions = { + InstanceId = "i-abc123" + } +} + +resource "aws_sns_topic" "test" { + count = 1 + name = "tf-test-alarms-${count.index}-%[1]s" +} + +resource "aws_cloudwatch_composite_alarm" "test" { + alarm_actions = aws_sns_topic.test.*.arn + alarm_description = "Test 1" + alarm_name = "tf-test-composite-%[1]s" + alarm_rule = join(" OR ", formatlist("ALARM(%%s)", aws_cloudwatch_metric_alarm.test.*.alarm_name)) + insufficient_data_actions = aws_sns_topic.test.*.arn + ok_actions = aws_sns_topic.test.*.arn + + tags = { + Foo = "Bar" + } +} +`, suffix) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_update(suffix string) string { + return fmt.Sprintf(` +resource "aws_cloudwatch_metric_alarm" "test" { + count = 2 + + alarm_name = "tf-test-metric-${count.index}-%[1]s" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = 2 + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = 120 + statistic = "Average" + threshold = 80 + + dimensions = { + InstanceId = "i-abc123" + } +} + +resource "aws_sns_topic" "test" { + count = 2 + name = "tf-test-alarms-${count.index}-%[1]s" +} + +resource "aws_cloudwatch_composite_alarm" "test" { + alarm_actions = aws_sns_topic.test.*.arn + alarm_description = "Test 2" + alarm_name = "tf-test-composite-%[1]s" + alarm_rule = "ALARM(${aws_cloudwatch_metric_alarm.test[0].alarm_name})" + insufficient_data_actions = aws_sns_topic.test.*.arn + ok_actions = aws_sns_topic.test.*.arn + + tags = { + Foo = "Bar" + Bax = "Baf" + } +} +`, suffix) +} + +func TestAccAwsCloudWatchCompositeAlarm_disappears(t *testing.T) { + alarm := cloudwatch.CompositeAlarm{} + suffix := acctest.RandString(8) + resourceName := "aws_cloudwatch_composite_alarm.test" + + checkDisappears := func(*terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudwatchconn + alarmNames := []string{ + "tf-test-composite-" + suffix, + "tf-test-metric-" + suffix, + } + + for _, name := range alarmNames { + input := cloudwatch.DeleteAlarmsInput{ + AlarmNames: []*string{&name}, + } + + _, err := conn.DeleteAlarms(&input) + if err != nil { + return err + } + } + + return nil + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsCloudWatchCompositeAlarmDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_disappears(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName, &alarm), + checkDisappears, + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_disappears(suffix string) string { + return fmt.Sprintf(` +resource "aws_cloudwatch_metric_alarm" "test" { + alarm_name = "tf-test-metric-%[1]s" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = 2 + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = 120 + statistic = "Average" + threshold = 80 + + dimensions = { + InstanceId = "i-abc123" + } +} + +resource "aws_cloudwatch_composite_alarm" "test" { + alarm_name = "tf-test-composite-%[1]s" + alarm_rule = "ALARM(${aws_cloudwatch_metric_alarm.test.alarm_name})" +} +`, suffix) +} diff --git a/website/docs/r/cloudwatch_composite_alarm.html.markdown b/website/docs/r/cloudwatch_composite_alarm.html.markdown new file mode 100644 index 000000000000..4a140d0f4656 --- /dev/null +++ b/website/docs/r/cloudwatch_composite_alarm.html.markdown @@ -0,0 +1,56 @@ +--- +subcategory: "CloudWatch" +layout: "aws" +page_title: "AWS: aws_cloudwatch_composite_alarm" +description: |- + Provides a CloudWatch Composite Alarm resource. +--- + +# Resource: aws_cloudwatch_composite_alarm + +Provides a CloudWatch Composite Alarm resource. + +~> **NOTE:** An alarm (composite or metric) cannot be destroyed when there are other composite alarms depending on it. This can lead to a cyclical dependency on update, as Terraform will unsuccessfully attempt to destroy alarms before updating the rule. Consider using `depends_on`, references to alarm names, and two-stage updates. + +## Example Usage + +```hcl +resource "aws_cloudwatch_composite_alarm" "example" { + alarm_description = "This is a composite alarm!" + alarm_name = "example-composite-alarm" + + alarm_actions = aws_sns_topic.example.arn + ok_actions = aws_sns_topic.example.arn + + alarm_rule = <