diff --git a/aws/internal/service/cloudwatch/finder/finder.go b/aws/internal/service/cloudwatch/finder/finder.go new file mode 100644 index 000000000000..1de5bf2c9d68 --- /dev/null +++ b/aws/internal/service/cloudwatch/finder/finder.go @@ -0,0 +1,26 @@ +package finder + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatch" +) + +func CompositeAlarmByName(ctx context.Context, conn *cloudwatch.CloudWatch, name string) (*cloudwatch.CompositeAlarm, error) { + input := cloudwatch.DescribeAlarmsInput{ + AlarmNames: aws.StringSlice([]string{name}), + AlarmTypes: aws.StringSlice([]string{cloudwatch.AlarmTypeCompositeAlarm}), + } + + output, err := conn.DescribeAlarmsWithContext(ctx, &input) + if err != nil { + return nil, err + } + + if output == nil || len(output.CompositeAlarms) != 1 { + return nil, nil + } + + return output.CompositeAlarms[0], nil +} 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..46ac4c36a432 --- /dev/null +++ b/aws/resource_aws_cloudwatch_composite_alarm.go @@ -0,0 +1,239 @@ +package aws + +import ( + "context" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatch/finder" +) + +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, + ForceNew: 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("error creating CloudWatch Composite Alarm (%s): %s", name, err) + } + + 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, err := finder.CompositeAlarmByName(ctx, conn, name) + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, cloudwatch.ErrCodeResourceNotFound) { + log.Printf("[WARN] CloudWatch Composite Alarm %s not found, removing from state", name) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("error reading CloudWatch Composite Alarm (%s): %s", name, err) + } + + if alarm == nil { + if d.IsNewResource() { + return diag.Errorf("error reading CloudWatch Composite Alarm (%s): not found", name) + } + + log.Printf("[WARN] CloudWatch Composite Alarm %s not found, removing from state", 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("error setting 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("error setting insufficient_data_actions: %s", err) + } + + if err := d.Set("ok_actions", flattenStringSet(alarm.OKActions)); err != nil { + return diag.Errorf("error setting ok_actions: %s", err) + } + + tags, err := keyvaluetags.CloudwatchListTags(conn, aws.StringValue(alarm.AlarmArn)) + if err != nil { + return diag.Errorf("error listing tags of alarm: %s", err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return diag.Errorf("error setting tags: %s", err) + } + + return nil +} + +func resourceAwsCloudWatchCompositeAlarmUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).cloudwatchconn + name := d.Id() + + input := expandAwsCloudWatchPutCompositeAlarmInput(d) + + _, err := conn.PutCompositeAlarmWithContext(ctx, &input) + if err != nil { + return diag.Errorf("error updating CloudWatch Composite Alarm (%s): %s", name, 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("error updating 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() + + input := cloudwatch.DeleteAlarmsInput{ + AlarmNames: aws.StringSlice([]string{name}), + } + + _, err := conn.DeleteAlarmsWithContext(ctx, &input) + if err != nil { + if tfawserr.ErrCodeEquals(err, cloudwatch.ErrCodeResourceNotFound) { + return nil + } + return diag.Errorf("error deleting CloudWatch Composite Alarm (%s): %s", name, err) + } + + return nil +} + +func expandAwsCloudWatchPutCompositeAlarmInput(d *schema.ResourceData) cloudwatch.PutCompositeAlarmInput { + out := cloudwatch.PutCompositeAlarmInput{ + ActionsEnabled: aws.Bool(d.Get("actions_enabled").(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 +} 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..baf9bd788add --- /dev/null +++ b/aws/resource_aws_cloudwatch_composite_alarm_test.go @@ -0,0 +1,676 @@ +package aws + +import ( + "context" + "fmt" + "log" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "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" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatch/finder" +) + +func init() { + resource.AddTestSweepers("aws_cloudwatch_composite_alarm", &resource.Sweeper{ + Name: "aws_cloudwatch_composite_alarm", + F: testSweepCloudWatchCompositeAlarms, + }) +} + +func testSweepCloudWatchCompositeAlarms(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + + conn := client.(*AWSClient).cloudwatchconn + ctx := context.Background() + + input := &cloudwatch.DescribeAlarmsInput{ + AlarmTypes: aws.StringSlice([]string{cloudwatch.AlarmTypeCompositeAlarm}), + } + + var sweeperErrs *multierror.Error + + err = conn.DescribeAlarmsPagesWithContext(ctx, input, func(page *cloudwatch.DescribeAlarmsOutput, isLast bool) bool { + if page == nil { + return !isLast + } + + for _, compositeAlarm := range page.CompositeAlarms { + if compositeAlarm == nil { + continue + } + + name := aws.StringValue(compositeAlarm.AlarmName) + + log.Printf("[INFO] Deleting CloudWatch Composite Alarm: %s", name) + + r := resourceAwsCloudWatchCompositeAlarm() + d := r.Data(nil) + d.SetId(name) + + diags := r.DeleteContext(ctx, d, client) + + for i := range diags { + if diags[i].Severity == diag.Error { + log.Printf("[ERROR] %s", diags[i].Summary) + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf(diags[i].Summary)) + continue + } + } + } + + return !isLast + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping CloudWatch Composite Alarms sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors + } + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error retrieving CloudWatch Composite Alarms: %w", err)) + } + + return sweeperErrs.ErrorOrNil() +} + +func TestAccAwsCloudWatchCompositeAlarm_basic(t *testing.T) { + 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_basic(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "actions_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "alarm_actions.#", "0"), + resource.TestCheckResourceAttr(resourceName, "alarm_description", ""), + 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.#", "0"), + resource.TestCheckResourceAttr(resourceName, "ok_actions.#", "0"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsCloudWatchCompositeAlarm_disappears(t *testing.T) { + 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_basic(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsCloudWatchCompositeAlarm(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsCloudWatchCompositeAlarm_actionsEnabled(t *testing.T) { + 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_actionsEnabled(false, suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "actions_enabled", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_actionsEnabled(true, suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "actions_enabled", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsCloudWatchCompositeAlarm_alarmActions(t *testing.T) { + 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_alarmActions(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "alarm_actions.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_updateAlarmActions(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "alarm_actions.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_basic(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "alarm_actions.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsCloudWatchCompositeAlarm_description(t *testing.T) { + 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_description("Test 1", suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "alarm_description", "Test 1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_description("Test Updated", suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "alarm_description", "Test Updated"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsCloudWatchCompositeAlarm_insufficientDataActions(t *testing.T) { + 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_insufficientDataActions(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "insufficient_data_actions.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_updateInsufficientDataActions(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "insufficient_data_actions.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_basic(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "insufficient_data_actions.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsCloudWatchCompositeAlarm_okActions(t *testing.T) { + 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_okActions(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "ok_actions.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_updateOkActions(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "ok_actions.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_basic(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "ok_actions.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsCloudWatchCompositeAlarm_allActions(t *testing.T) { + 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_allActions(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "alarm_actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "insufficient_data_actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "ok_actions.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_basic(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "alarm_actions.#", "0"), + resource.TestCheckResourceAttr(resourceName, "insufficient_data_actions.#", "0"), + resource.TestCheckResourceAttr(resourceName, "ok_actions.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsCloudWatchCompositeAlarm_updateAlarmRule(t *testing.T) { + 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_basic(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsCloudWatchCompositeAlarmConfig_updateAlarmRule(suffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "alarm_rule", fmt.Sprintf("ALARM(tf-test-metric-0-%[1]s)", suffix)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +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 + } + + alarm, err := finder.CompositeAlarmByName(context.Background(), conn, rs.Primary.ID) + + if tfawserr.ErrCodeEquals(err, cloudwatch.ErrCodeResourceNotFound) { + continue + } + if err != nil { + return fmt.Errorf("error reading CloudWatch composite alarm (%s): %w", rs.Primary.ID, err) + } + + if alarm != nil { + return fmt.Errorf("CloudWatch composite alarm (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckAwsCloudWatchCompositeAlarmExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("resource %s has not set its id", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).cloudwatchconn + + alarm, err := finder.CompositeAlarmByName(context.Background(), conn, rs.Primary.ID) + + if err != nil { + return fmt.Errorf("error reading CloudWatch composite alarm (%s): %w", rs.Primary.ID, err) + } + + if alarm == nil { + return fmt.Errorf("CloudWatch composite alarm (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccAwsCloudWatchCompositeAlarmBaseConfig(suffix string) string { + return fmt.Sprintf(` +resource "aws_cloudwatch_metric_alarm" "test" { + count = 2 + + alarm_name = "tf-test-metric-${count.index}-%s" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = 2 + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = 120 + statistic = "Average" + threshold = 80 + + dimensions = { + InstanceId = "i-abc123" + } +} +`, suffix) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_actionsEnabled(enabled bool, suffix string) string { + return composeConfig( + testAccAwsCloudWatchCompositeAlarmBaseConfig(suffix), + fmt.Sprintf(` +resource "aws_cloudwatch_composite_alarm" "test" { + actions_enabled = %t + alarm_name = "tf-test-composite-%s" + alarm_rule = join(" OR ", formatlist("ALARM(%%s)", aws_cloudwatch_metric_alarm.test.*.alarm_name)) +} +`, enabled, suffix)) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_basic(suffix string) string { + return composeConfig( + testAccAwsCloudWatchCompositeAlarmBaseConfig(suffix), + fmt.Sprintf(` +resource "aws_cloudwatch_composite_alarm" "test" { + alarm_name = "tf-test-composite-%[1]s" + alarm_rule = join(" OR ", formatlist("ALARM(%%s)", aws_cloudwatch_metric_alarm.test.*.alarm_name)) +} +`, suffix)) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_description(description, suffix string) string { + return composeConfig( + testAccAwsCloudWatchCompositeAlarmBaseConfig(suffix), + fmt.Sprintf(` +resource "aws_cloudwatch_composite_alarm" "test" { + alarm_description = %q + alarm_name = "tf-test-composite-%s" + alarm_rule = join(" OR ", formatlist("ALARM(%%s)", aws_cloudwatch_metric_alarm.test.*.alarm_name)) +} +`, description, suffix)) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_alarmActions(suffix string) string { + return composeConfig( + testAccAwsCloudWatchCompositeAlarmBaseConfig(suffix), + fmt.Sprintf(` +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_name = "tf-test-composite-%[1]s" + alarm_rule = "ALARM(${aws_cloudwatch_metric_alarm.test[0].alarm_name})" +} +`, suffix)) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_updateAlarmActions(suffix string) string { + return composeConfig( + testAccAwsCloudWatchCompositeAlarmBaseConfig(suffix), + fmt.Sprintf(` +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[0].arn] + alarm_name = "tf-test-composite-%[1]s" + alarm_rule = "ALARM(${aws_cloudwatch_metric_alarm.test[0].alarm_name})" +} +`, suffix)) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_updateAlarmRule(suffix string) string { + return composeConfig( + testAccAwsCloudWatchCompositeAlarmBaseConfig(suffix), + fmt.Sprintf(` +resource "aws_cloudwatch_composite_alarm" "test" { + alarm_name = "tf-test-composite-%[1]s" + alarm_rule = "ALARM(${aws_cloudwatch_metric_alarm.test[0].alarm_name})" +} +`, suffix)) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_insufficientDataActions(suffix string) string { + return composeConfig( + testAccAwsCloudWatchCompositeAlarmBaseConfig(suffix), + fmt.Sprintf(` +resource "aws_sns_topic" "test" { + count = 2 + name = "tf-test-alarms-${count.index}-%[1]s" +} + +resource "aws_cloudwatch_composite_alarm" "test" { + 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 +} +`, suffix)) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_updateInsufficientDataActions(suffix string) string { + return composeConfig( + testAccAwsCloudWatchCompositeAlarmBaseConfig(suffix), + fmt.Sprintf(` +resource "aws_sns_topic" "test" { + count = 2 + name = "tf-test-alarms-${count.index}-%[1]s" +} + +resource "aws_cloudwatch_composite_alarm" "test" { + 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[0].arn] +} +`, suffix)) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_okActions(suffix string) string { + return composeConfig( + testAccAwsCloudWatchCompositeAlarmBaseConfig(suffix), + fmt.Sprintf(` +resource "aws_sns_topic" "test" { + count = 2 + name = "tf-test-alarms-${count.index}-%[1]s" +} + +resource "aws_cloudwatch_composite_alarm" "test" { + alarm_name = "tf-test-composite-%[1]s" + alarm_rule = "ALARM(${aws_cloudwatch_metric_alarm.test[0].alarm_name})" + ok_actions = aws_sns_topic.test.*.arn +} +`, suffix)) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_updateOkActions(suffix string) string { + return composeConfig( + testAccAwsCloudWatchCompositeAlarmBaseConfig(suffix), + fmt.Sprintf(` +resource "aws_sns_topic" "test" { + count = 2 + name = "tf-test-alarms-${count.index}-%[1]s" +} + +resource "aws_cloudwatch_composite_alarm" "test" { + alarm_name = "tf-test-composite-%[1]s" + alarm_rule = "ALARM(${aws_cloudwatch_metric_alarm.test[0].alarm_name})" + ok_actions = [aws_sns_topic.test[0].arn] +} +`, suffix)) +} + +func testAccAwsCloudWatchCompositeAlarmConfig_allActions(suffix string) string { + return composeConfig( + testAccAwsCloudWatchCompositeAlarmBaseConfig(suffix), + fmt.Sprintf(` +resource "aws_sns_topic" "test" { + count = 3 + name = "tf-test-alarms-${count.index}-%[1]s" +} + +resource "aws_cloudwatch_composite_alarm" "test" { + alarm_actions = [aws_sns_topic.test[0].arn] + 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[1].arn] + ok_actions = [aws_sns_topic.test[2].arn] +} +`, 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..0d74e77c16d8 --- /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 = <