diff --git a/.changelog/20312.txt b/.changelog/20312.txt new file mode 100644 index 00000000000..32e20365f6d --- /dev/null +++ b/.changelog/20312.txt @@ -0,0 +1,7 @@ +```release-note:bug +aws/resource_aws_cloudwatch_event_rule: Correctly handle ARN in `event_bus_name` argument +``` + +```release-note:bug +aws/resource_aws_cloudwatch_event_target: Correctly handle ARN in `event_bus_name` argument +``` \ No newline at end of file diff --git a/aws/internal/service/cloudwatchevents/enum.go b/aws/internal/service/cloudwatchevents/enum.go new file mode 100644 index 00000000000..a4ed557c839 --- /dev/null +++ b/aws/internal/service/cloudwatchevents/enum.go @@ -0,0 +1,5 @@ +package cloudwatchevents + +const ( + DefaultEventBusName = "default" +) diff --git a/aws/internal/service/cloudwatchevents/finder/finder.go b/aws/internal/service/cloudwatchevents/finder/finder.go index 408979e14e7..65dcf4459d7 100644 --- a/aws/internal/service/cloudwatchevents/finder/finder.go +++ b/aws/internal/service/cloudwatchevents/finder/finder.go @@ -39,25 +39,46 @@ func ConnectionByName(conn *events.CloudWatchEvents, name string) (*events.Descr return output, nil } -func Rule(conn *events.CloudWatchEvents, eventBusName, ruleName string) (*events.DescribeRuleOutput, error) { +func RuleByEventBusAndRuleNames(conn *events.CloudWatchEvents, eventBusName, ruleName string) (*events.DescribeRuleOutput, error) { input := events.DescribeRuleInput{ Name: aws.String(ruleName), } + if eventBusName != "" { input.EventBusName = aws.String(eventBusName) } - return conn.DescribeRule(&input) + output, err := conn.DescribeRule(&input) + if tfawserr.ErrCodeEquals(err, events.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil } -func RuleByID(conn *events.CloudWatchEvents, ruleID string) (*events.DescribeRuleOutput, error) { - busName, ruleName, err := tfevents.RuleParseID(ruleID) +func RuleByResourceID(conn *events.CloudWatchEvents, id string) (*events.DescribeRuleOutput, error) { + eventBusName, ruleName, err := tfevents.RuleParseResourceID(id) + if err != nil { return nil, err } - return Rule(conn, busName, ruleName) + return RuleByEventBusAndRuleNames(conn, eventBusName, ruleName) } func Target(conn *events.CloudWatchEvents, busName, ruleName, targetId string) (*events.Target, error) { diff --git a/aws/internal/service/cloudwatchevents/id.go b/aws/internal/service/cloudwatchevents/id.go index 7d383458c5a..34a73a59a70 100644 --- a/aws/internal/service/cloudwatchevents/id.go +++ b/aws/internal/service/cloudwatchevents/id.go @@ -7,22 +7,26 @@ import ( ) var ( + eventBusARNPattern = regexp.MustCompile(`^arn:aws[\w-]*:events:[a-z]{2}-[a-z]+-[\w-]+:[0-9]{12}:event-bus\/[\.\-_A-Za-z0-9]+$`) partnerEventBusPattern = regexp.MustCompile(`^aws\.partner(/[\.\-_A-Za-z0-9]+){2,}$`) ) -const DefaultEventBusName = "default" +const permissionResourceIDSeparator = "/" -const PermissionIDSeparator = "/" - -func PermissionCreateID(eventBusName, statementID string) string { +func PermissionCreateResourceID(eventBusName, statementID string) string { if eventBusName == "" || eventBusName == DefaultEventBusName { return statementID } - return eventBusName + PermissionIDSeparator + statementID + + parts := []string{eventBusName, statementID} + id := strings.Join(parts, permissionResourceIDSeparator) + + return id } -func PermissionParseID(id string) (string, string, error) { - parts := strings.Split(id, PermissionIDSeparator) +func PermissionParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, permissionResourceIDSeparator) + if len(parts) == 1 && parts[0] != "" { return DefaultEventBusName, parts[0], nil } @@ -30,20 +34,25 @@ func PermissionParseID(id string) (string, string, error) { return parts[0], parts[1], nil } - return "", "", fmt.Errorf("unexpected format for ID (%q), expected "+PermissionIDSeparator+" or ", id) + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected EVENTBUSNAME%[2]sSTATEMENTID or STATEMENTID", id, permissionResourceIDSeparator) } -const ruleIDSeparator = "/" +const ruleResourceIDSeparator = "/" -func RuleCreateID(eventBusName, ruleName string) string { +func RuleCreateResourceID(eventBusName, ruleName string) string { if eventBusName == "" || eventBusName == DefaultEventBusName { return ruleName } - return eventBusName + ruleIDSeparator + ruleName + + parts := []string{eventBusName, ruleName} + id := strings.Join(parts, ruleResourceIDSeparator) + + return id } -func RuleParseID(id string) (string, string, error) { - parts := strings.Split(id, ruleIDSeparator) +func RuleParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, ruleResourceIDSeparator) + if len(parts) == 1 && parts[0] != "" { return DefaultEventBusName, parts[0], nil } @@ -51,33 +60,42 @@ func RuleParseID(id string) (string, string, error) { return parts[0], parts[1], nil } if len(parts) > 2 { - i := strings.LastIndex(id, ruleIDSeparator) - busName := id[:i] - statementID := id[i+1:] - if partnerEventBusPattern.MatchString(busName) && statementID != "" { - return busName, statementID, nil + i := strings.LastIndex(id, ruleResourceIDSeparator) + eventBusName := id[:i] + ruleName := id[i+1:] + if eventBusARNPattern.MatchString(eventBusName) && ruleName != "" { + return eventBusName, ruleName, nil + } + if partnerEventBusPattern.MatchString(eventBusName) && ruleName != "" { + return eventBusName, ruleName, nil } } - return "", "", fmt.Errorf("unexpected format for ID (%q), expected "+ruleIDSeparator+" or ", id) + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected EVENTBUSNAME%[2]sRULENAME or RULENAME", id, ruleResourceIDSeparator) } -// Terraform state IDs for Targets are not parseable, since the separator used ("-") is also a valid -// character in both the rule name and the target id. +// Terraform resource IDs for Targets are not parseable as the separator used ("-") is also a valid character in both the rule name and the target ID. -const targetIDSeparator = "-" +const targetResourceIDSeparator = "-" const targetImportIDSeparator = "/" -func TargetCreateID(eventBusName, ruleName, targetID string) string { - id := ruleName + targetIDSeparator + targetID - if eventBusName != "" && eventBusName != DefaultEventBusName { - id = eventBusName + targetIDSeparator + id +func TargetCreateResourceID(eventBusName, ruleName, targetID string) string { + var parts []string + + if eventBusName == "" || eventBusName == DefaultEventBusName { + parts = []string{ruleName, targetID} + } else { + parts = []string{eventBusName, ruleName, targetID} } + + id := strings.Join(parts, targetResourceIDSeparator) + return id } func TargetParseImportID(id string) (string, string, string, error) { parts := strings.Split(id, targetImportIDSeparator) + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { return DefaultEventBusName, parts[0], parts[1], nil } @@ -88,12 +106,15 @@ func TargetParseImportID(id string) (string, string, string, error) { iTarget := strings.LastIndex(id, targetImportIDSeparator) targetID := id[iTarget+1:] iRule := strings.LastIndex(id[:iTarget], targetImportIDSeparator) - busName := id[:iRule] + eventBusName := id[:iRule] ruleName := id[iRule+1 : iTarget] - if partnerEventBusPattern.MatchString(busName) && ruleName != "" && targetID != "" { - return busName, ruleName, targetID, nil + if eventBusARNPattern.MatchString(eventBusName) && ruleName != "" && targetID != "" { + return eventBusName, ruleName, targetID, nil + } + if partnerEventBusPattern.MatchString(eventBusName) && ruleName != "" && targetID != "" { + return eventBusName, ruleName, targetID, nil } } - return "", "", "", fmt.Errorf("unexpected format for ID (%q), expected "+targetImportIDSeparator+""+targetImportIDSeparator+" or "+targetImportIDSeparator+"", id) + return "", "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected EVENTBUSNAME%[2]sRULENAME%[2]sTARGETID or RULENAME%[2]sTARGETID", id, targetImportIDSeparator) } diff --git a/aws/internal/service/cloudwatchevents/id_test.go b/aws/internal/service/cloudwatchevents/id_test.go index 97ae7823a90..95009010463 100644 --- a/aws/internal/service/cloudwatchevents/id_test.go +++ b/aws/internal/service/cloudwatchevents/id_test.go @@ -6,7 +6,7 @@ import ( tfevents "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents" ) -func TestPermissionParseID(t *testing.T) { +func TestPermissionParseResourceID(t *testing.T) { testCases := []struct { TestName string InputID string @@ -27,13 +27,13 @@ func TestPermissionParseID(t *testing.T) { }, { TestName: "two parts", - InputID: tfevents.PermissionCreateID("TestEventBus", "TestStatement"), + InputID: tfevents.PermissionCreateResourceID("TestEventBus", "TestStatement"), ExpectedPart0: "TestEventBus", ExpectedPart1: "TestStatement", }, { TestName: "two parts with default event bus", - InputID: tfevents.PermissionCreateID(tfevents.DefaultEventBusName, "TestStatement"), + InputID: tfevents.PermissionCreateResourceID(tfevents.DefaultEventBusName, "TestStatement"), ExpectedPart0: tfevents.DefaultEventBusName, ExpectedPart1: "TestStatement", }, @@ -66,7 +66,7 @@ func TestPermissionParseID(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.TestName, func(t *testing.T) { - gotPart0, gotPart1, err := tfevents.PermissionParseID(testCase.InputID) + gotPart0, gotPart1, err := tfevents.PermissionParseResourceID(testCase.InputID) if err == nil && testCase.ExpectedError { t.Fatalf("expected error, got no error") @@ -87,7 +87,7 @@ func TestPermissionParseID(t *testing.T) { } } -func TestRuleParseID(t *testing.T) { +func TestRuleParseResourceID(t *testing.T) { testCases := []struct { TestName string InputID string @@ -108,22 +108,34 @@ func TestRuleParseID(t *testing.T) { }, { TestName: "two parts", - InputID: tfevents.RuleCreateID("TestEventBus", "TestRule"), + InputID: tfevents.RuleCreateResourceID("TestEventBus", "TestRule"), ExpectedPart0: "TestEventBus", ExpectedPart1: "TestRule", }, { TestName: "two parts with default event bus", - InputID: tfevents.RuleCreateID(tfevents.DefaultEventBusName, "TestRule"), + InputID: tfevents.RuleCreateResourceID(tfevents.DefaultEventBusName, "TestRule"), ExpectedPart0: tfevents.DefaultEventBusName, ExpectedPart1: "TestRule", }, { - TestName: "partner event bus", + TestName: "partner event bus 1", InputID: "aws.partner/example.com/Test/TestRule", ExpectedPart0: "aws.partner/example.com/Test", ExpectedPart1: "TestRule", }, + { + TestName: "partner event bus 2", + InputID: "aws.partner/foo.com/foo/18554d09-58ff-aa42-ba9c-c4c33899006f/test", + ExpectedPart0: "aws.partner/foo.com/foo/18554d09-58ff-aa42-ba9c-c4c33899006f", + ExpectedPart1: "test", + }, + { + TestName: "ARN event bus", + InputID: tfevents.RuleCreateResourceID("arn:aws:events:us-east-2:123456789012:event-bus/default", "TestRule"), + ExpectedPart0: "arn:aws:events:us-east-2:123456789012:event-bus/default", + ExpectedPart1: "TestRule", + }, { TestName: "empty both parts", InputID: "/", @@ -163,7 +175,7 @@ func TestRuleParseID(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.TestName, func(t *testing.T) { - gotPart0, gotPart1, err := tfevents.RuleParseID(testCase.InputID) + gotPart0, gotPart1, err := tfevents.RuleParseResourceID(testCase.InputID) if err == nil && testCase.ExpectedError { t.Fatalf("expected error, got no error") @@ -281,6 +293,13 @@ func TestTargetParseImportID(t *testing.T) { ExpectedPart1: "TestRule", ExpectedPart2: "TestTarget", }, + { + TestName: "ARN event bus", + InputID: "arn:aws:events:us-east-2:123456789012:event-bus/default/TestRule/TestTarget", + ExpectedPart0: "arn:aws:events:us-east-2:123456789012:event-bus/default", + ExpectedPart1: "TestRule", + ExpectedPart2: "TestTarget", + }, { TestName: "empty partner event rule and target", InputID: "aws.partner/example.com/Test//", diff --git a/aws/internal/service/cloudwatchevents/state.go b/aws/internal/service/cloudwatchevents/state.go new file mode 100644 index 00000000000..915ff9618b9 --- /dev/null +++ b/aws/internal/service/cloudwatchevents/state.go @@ -0,0 +1,31 @@ +package cloudwatchevents + +import ( + "fmt" + + events "github.com/aws/aws-sdk-go/service/cloudwatchevents" +) + +// RuleEnabledFromState infers from its state whether or not a rule is enabled. +func RuleEnabledFromState(state string) (bool, error) { + if state == events.RuleStateEnabled { + return true, nil + } + + if state == events.RuleStateDisabled { + return false, nil + } + + // We don't just blindly trust AWS as they tend to return + // unexpected values in similar cases (different casing etc.) + return false, fmt.Errorf("unable to infer enabled from state: %s", state) +} + +// RuleStateFromEnabled returns a rule's state based on whether or not it is enabled. +func RuleStateFromEnabled(enabled bool) string { + if enabled { + return events.RuleStateEnabled + } + + return events.RuleStateDisabled +} diff --git a/aws/internal/service/cloudwatchevents/state_test.go b/aws/internal/service/cloudwatchevents/state_test.go new file mode 100644 index 00000000000..966c95cca80 --- /dev/null +++ b/aws/internal/service/cloudwatchevents/state_test.go @@ -0,0 +1,83 @@ +package cloudwatchevents_test + +import ( + "testing" + + tfevents "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents" +) + +func TestRuleEnabledFromState(t *testing.T) { + testCases := []struct { + TestName string + State string + ExpectedError bool + ExpectedEnabled bool + }{ + { + TestName: "empty state", + ExpectedError: true, + }, + { + TestName: "invalid state", + State: "UNKNOWN", + ExpectedError: true, + }, + { + TestName: "enabled", + State: "ENABLED", + ExpectedEnabled: true, + }, + { + TestName: "disabled", + State: "DISABLED", + ExpectedEnabled: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + gotEnabled, err := tfevents.RuleEnabledFromState(testCase.State) + + if err == nil && testCase.ExpectedError { + t.Fatalf("expected error, got no error") + } + + if err != nil && !testCase.ExpectedError { + t.Fatalf("got unexpected error: %s", err) + } + + if gotEnabled != testCase.ExpectedEnabled { + t.Errorf("got enabled %t, expected %t", gotEnabled, testCase.ExpectedEnabled) + } + }) + } +} + +func RuleStateFromEnabled(t *testing.T) { + testCases := []struct { + TestName string + Enabled bool + ExpectedState string + }{ + { + TestName: "enabled", + Enabled: true, + ExpectedState: "ENABLED", + }, + { + TestName: "disabled", + Enabled: false, + ExpectedState: "DISABLED", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + gotState := tfevents.RuleStateFromEnabled(testCase.Enabled) + + if gotState != testCase.ExpectedState { + t.Errorf("got enabled %s, expected %s", gotState, testCase.ExpectedState) + } + }) + } +} diff --git a/aws/resource_aws_cloudwatch_event_archive_test.go b/aws/resource_aws_cloudwatch_event_archive_test.go index 214edcfadcd..43f82a6af65 100644 --- a/aws/resource_aws_cloudwatch_event_archive_test.go +++ b/aws/resource_aws_cloudwatch_event_archive_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudwatchevents" events "github.com/aws/aws-sdk-go/service/cloudwatchevents" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -78,7 +77,7 @@ func TestAccAWSCloudWatchEventArchive_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventArchiveDestroy, Steps: []resource.TestStep{ @@ -109,7 +108,7 @@ func TestAccAWSCloudWatchEventArchive_update(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventArchiveDestroy, Steps: []resource.TestStep{ @@ -139,7 +138,7 @@ func TestAccAWSCloudWatchEventArchive_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventArchiveDestroy, Steps: []resource.TestStep{ @@ -211,7 +210,7 @@ func TestAccAWSCloudWatchEventArchive_retentionSetOnCreation(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventArchiveDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_cloudwatch_event_permission.go b/aws/resource_aws_cloudwatch_event_permission.go index d97cdd5769e..d3c7fdfef65 100644 --- a/aws/resource_aws_cloudwatch_event_permission.go +++ b/aws/resource_aws_cloudwatch_event_permission.go @@ -100,7 +100,7 @@ func resourceAwsCloudWatchEventPermissionCreate(d *schema.ResourceData, meta int return fmt.Errorf("Creating CloudWatch Events permission failed: %w", err) } - id := tfevents.PermissionCreateID(eventBusName, statementID) + id := tfevents.PermissionCreateResourceID(eventBusName, statementID) d.SetId(id) return resourceAwsCloudWatchEventPermissionRead(d, meta) @@ -110,7 +110,7 @@ func resourceAwsCloudWatchEventPermissionCreate(d *schema.ResourceData, meta int func resourceAwsCloudWatchEventPermissionRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cloudwatcheventsconn - eventBusName, statementID, err := tfevents.PermissionParseID(d.Id()) + eventBusName, statementID, err := tfevents.PermissionParseResourceID(d.Id()) if err != nil { return fmt.Errorf("error reading CloudWatch Events permission (%s): %w", d.Id(), err) } @@ -207,7 +207,7 @@ func getPolicyStatement(output *events.DescribeEventBusOutput, statementID strin func resourceAwsCloudWatchEventPermissionUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cloudwatcheventsconn - eventBusName, statementID, err := tfevents.PermissionParseID(d.Id()) + eventBusName, statementID, err := tfevents.PermissionParseResourceID(d.Id()) if err != nil { return fmt.Errorf("error updating CloudWatch Events permission (%s): %w", d.Id(), err) } @@ -236,7 +236,7 @@ func resourceAwsCloudWatchEventPermissionUpdate(d *schema.ResourceData, meta int func resourceAwsCloudWatchEventPermissionDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cloudwatcheventsconn - eventBusName, statementID, err := tfevents.PermissionParseID(d.Id()) + eventBusName, statementID, err := tfevents.PermissionParseResourceID(d.Id()) if err != nil { return fmt.Errorf("error deleting CloudWatch Events permission (%s): %w", d.Id(), err) } diff --git a/aws/resource_aws_cloudwatch_event_permission_test.go b/aws/resource_aws_cloudwatch_event_permission_test.go index 153309ab038..4d01f5fca7d 100644 --- a/aws/resource_aws_cloudwatch_event_permission_test.go +++ b/aws/resource_aws_cloudwatch_event_permission_test.go @@ -9,7 +9,6 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudwatchevents" events "github.com/aws/aws-sdk-go/service/cloudwatchevents" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -76,7 +75,7 @@ func TestAccAWSCloudWatchEventPermission_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckCloudWatchEventPermissionDestroy, Steps: []resource.TestStep{ @@ -148,7 +147,7 @@ func TestAccAWSCloudWatchEventPermission_EventBusName(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckCloudWatchEventPermissionDestroy, Steps: []resource.TestStep{ @@ -179,7 +178,7 @@ func TestAccAWSCloudWatchEventPermission_Action(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckCloudWatchEventPermissionDestroy, Steps: []resource.TestStep{ @@ -221,7 +220,7 @@ func TestAccAWSCloudWatchEventPermission_Condition(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckCloudWatchEventPermissionDestroy, Steps: []resource.TestStep{ @@ -264,7 +263,7 @@ func TestAccAWSCloudWatchEventPermission_Multiple(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckCloudWatchEventPermissionDestroy, Steps: []resource.TestStep{ @@ -298,7 +297,7 @@ func TestAccAWSCloudWatchEventPermission_Disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckCloudWatchEventPermissionDestroy, Steps: []resource.TestStep{ @@ -326,7 +325,7 @@ func testAccCheckCloudWatchEventPermissionExists(pr string) resource.TestCheckFu return fmt.Errorf("No ID is set") } - eventBusName, statementID, err := tfevents.PermissionParseID(rs.Primary.ID) + eventBusName, statementID, err := tfevents.PermissionParseResourceID(rs.Primary.ID) if err != nil { return fmt.Errorf("error reading CloudWatch Events permission (%s): %w", pr, err) } @@ -361,7 +360,7 @@ func testAccCheckCloudWatchEventPermissionDestroy(s *terraform.State) error { continue } - eventBusName, statementID, err := tfevents.PermissionParseID(rs.Primary.ID) + eventBusName, statementID, err := tfevents.PermissionParseResourceID(rs.Primary.ID) if err != nil { return fmt.Errorf("error reading CloudWatch Events permission (%s): %w", rs.Primary.ID, err) } diff --git a/aws/resource_aws_cloudwatch_event_rule.go b/aws/resource_aws_cloudwatch_event_rule.go index a3c3f2d263b..d37717782db 100644 --- a/aws/resource_aws_cloudwatch_event_rule.go +++ b/aws/resource_aws_cloudwatch_event_rule.go @@ -17,6 +17,7 @@ import ( tfevents "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents/finder" iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -29,6 +30,7 @@ func resourceAwsCloudWatchEventRule() *schema.Resource { Read: resourceAwsCloudWatchEventRuleRead, Update: resourceAwsCloudWatchEventRuleUpdate, Delete: resourceAwsCloudWatchEventRuleDelete, + Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, @@ -108,8 +110,9 @@ func resourceAwsCloudWatchEventRuleCreate(d *schema.ResourceData, meta interface name := naming.Generate(d.Get("name").(string), d.Get("name_prefix").(string)) input, err := buildPutRuleInputStruct(d, name) + if err != nil { - return fmt.Errorf("Creating CloudWatch Events Rule failed: %w", err) + return err } if len(tags) > 0 { @@ -117,35 +120,30 @@ func resourceAwsCloudWatchEventRuleCreate(d *schema.ResourceData, meta interface } log.Printf("[DEBUG] Creating CloudWatch Events Rule: %s", input) - // IAM Roles take some time to propagate - var out *events.PutRuleOutput err = resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { - out, err = conn.PutRule(input) + _, err = conn.PutRule(input) - if isAWSErr(err, "ValidationException", "cannot be assumed by principal") { - log.Printf("[DEBUG] Retrying update of CloudWatch Events Rule %q", aws.StringValue(input.Name)) + if tfawserr.ErrMessageContains(err, "ValidationException", "cannot be assumed by principal") { return resource.RetryableError(err) } + if err != nil { return resource.NonRetryableError(err) } + return nil }) - if isResourceTimeoutError(err) { - out, err = conn.PutRule(input) + + if tfresource.TimedOut(err) { + _, err = conn.PutRule(input) } if err != nil { - return fmt.Errorf("Creating CloudWatch Events Rule failed: %w", err) + return fmt.Errorf("error creating CloudWatch Events Rule (%s): %w", name, err) } - d.Set("arn", out.RuleArn) - - id := tfevents.RuleCreateID(aws.StringValue(input.EventBusName), aws.StringValue(input.Name)) - d.SetId(id) - - log.Printf("[INFO] CloudWatch Events Rule (%s) created", aws.StringValue(out.RuleArn)) + d.SetId(tfevents.RuleCreateResourceID(aws.StringValue(input.EventBusName), aws.StringValue(input.Name))) return resourceAwsCloudWatchEventRuleRead(d, meta) } @@ -155,39 +153,47 @@ func resourceAwsCloudWatchEventRuleRead(d *schema.ResourceData, meta interface{} defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - out, err := finder.RuleByID(conn, d.Id()) - if tfawserr.ErrCodeEquals(err, events.ErrCodeResourceNotFoundException) { - log.Printf("[WARN] Removing CloudWatch Events Rule (%s) because it's gone.", d.Id()) + eventBusName, ruleName, err := tfevents.RuleParseResourceID(d.Id()) + + if err != nil { + return err + } + + output, err := finder.RuleByEventBusAndRuleNames(conn, eventBusName, ruleName) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] CloudWatch Events Rule (%s) not found, removing from state", d.Id()) d.SetId("") return nil } + if err != nil { return fmt.Errorf("error reading CloudWatch Events Rule (%s): %w", d.Id(), err) } - log.Printf("[DEBUG] Found Event Rule: %s", out) - arn := aws.StringValue(out.Arn) + arn := aws.StringValue(output.Arn) d.Set("arn", arn) - d.Set("description", out.Description) - if out.EventPattern != nil { - pattern, err := structure.NormalizeJsonString(aws.StringValue(out.EventPattern)) + d.Set("description", output.Description) + if output.EventPattern != nil { + pattern, err := structure.NormalizeJsonString(aws.StringValue(output.EventPattern)) if err != nil { return fmt.Errorf("event pattern contains an invalid JSON: %w", err) } d.Set("event_pattern", pattern) } - d.Set("name", out.Name) - d.Set("name_prefix", naming.NamePrefixFromName(aws.StringValue(out.Name))) - d.Set("role_arn", out.RoleArn) - d.Set("schedule_expression", out.ScheduleExpression) - d.Set("event_bus_name", out.EventBusName) + d.Set("name", output.Name) + d.Set("name_prefix", naming.NamePrefixFromName(aws.StringValue(output.Name))) + d.Set("role_arn", output.RoleArn) + d.Set("schedule_expression", output.ScheduleExpression) + d.Set("event_bus_name", eventBusName) // Use event bus name from resource ID as API response may collapse any ARN. + + enabled, err := tfevents.RuleEnabledFromState(aws.StringValue(output.State)) - boolState, err := getBooleanStateFromString(*out.State) if err != nil { return err } - log.Printf("[DEBUG] Setting boolean state: %t", boolState) - d.Set("is_enabled", boolState) + + d.Set("is_enabled", enabled) tags, err := keyvaluetags.CloudwatcheventsListTags(conn, arn) @@ -211,35 +217,40 @@ func resourceAwsCloudWatchEventRuleRead(d *schema.ResourceData, meta interface{} func resourceAwsCloudWatchEventRuleUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cloudwatcheventsconn - _, ruleName, err := tfevents.RuleParseID(d.Id()) + + _, ruleName, err := tfevents.RuleParseResourceID(d.Id()) + if err != nil { return err } + input, err := buildPutRuleInputStruct(d, ruleName) + if err != nil { - return fmt.Errorf("Updating CloudWatch Events Rule (%s) failed: %w", ruleName, err) + return err } - log.Printf("[DEBUG] Updating CloudWatch Events Rule: %s", input) // IAM Roles take some time to propagate err = resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { _, err := conn.PutRule(input) - if isAWSErr(err, "ValidationException", "cannot be assumed by principal") { - log.Printf("[DEBUG] Retrying update of CloudWatch Events Rule %q", aws.StringValue(input.Name)) + if tfawserr.ErrMessageContains(err, "ValidationException", "cannot be assumed by principal") { return resource.RetryableError(err) } + if err != nil { return resource.NonRetryableError(err) } + return nil }) - if isResourceTimeoutError(err) { + + if tfresource.TimedOut(err) { _, err = conn.PutRule(input) } if err != nil { - return fmt.Errorf("Updating CloudWatch Events Rule (%s) failed: %w", ruleName, err) + return fmt.Errorf("error updating CloudWatch Events Rule (%s): %w", d.Id(), err) } arn := d.Get("arn").(string) @@ -256,19 +267,25 @@ func resourceAwsCloudWatchEventRuleUpdate(d *schema.ResourceData, meta interface func resourceAwsCloudWatchEventRuleDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cloudwatcheventsconn - busName, ruleName, err := tfevents.RuleParseID(d.Id()) + + eventBusName, ruleName, err := tfevents.RuleParseResourceID(d.Id()) + if err != nil { return err } + input := &events.DeleteRuleInput{ - Name: aws.String(ruleName), - EventBusName: aws.String(busName), + Name: aws.String(ruleName), + } + if eventBusName != "" { + input.EventBusName = aws.String(eventBusName) } + log.Printf("[DEBUG] Deleting CloudWatch Events Rule: %s", d.Id()) err = resource.Retry(cloudWatchEventRuleDeleteRetryTimeout, func() *resource.RetryError { _, err := conn.DeleteRule(input) - if isAWSErr(err, "ValidationException", "Rule can't be deleted since it has targets") { + if tfawserr.ErrMessageContains(err, "ValidationException", "Rule can't be deleted since it has targets") { return resource.RetryableError(err) } @@ -279,10 +296,14 @@ func resourceAwsCloudWatchEventRuleDelete(d *schema.ResourceData, meta interface return nil }) - if isResourceTimeoutError(err) { + if tfresource.TimedOut(err) { _, err = conn.DeleteRule(input) } + if tfawserr.ErrCodeEquals(err, events.ErrCodeResourceNotFoundException) { + return nil + } + if err != nil { return fmt.Errorf("error deleting CloudWatch Events Rule (%s): %w", d.Id(), err) } @@ -316,31 +337,11 @@ func buildPutRuleInputStruct(d *schema.ResourceData, name string) (*events.PutRu input.ScheduleExpression = aws.String(v.(string)) } - input.State = aws.String(getStringStateFromBoolean(d.Get("is_enabled").(bool))) + input.State = aws.String(tfevents.RuleStateFromEnabled(d.Get("is_enabled").(bool))) return &input, nil } -// State is represented as (ENABLED|DISABLED) in the API -func getBooleanStateFromString(state string) (bool, error) { - if state == events.RuleStateEnabled { - return true, nil - } else if state == events.RuleStateDisabled { - return false, nil - } - // We don't just blindly trust AWS as they tend to return - // unexpected values in similar cases (different casing etc.) - return false, fmt.Errorf("Failed converting state %q into boolean", state) -} - -// State is represented as (ENABLED|DISABLED) in the API -func getStringStateFromBoolean(isEnabled bool) string { - if isEnabled { - return events.RuleStateEnabled - } - return events.RuleStateDisabled -} - func validateEventPatternValue() schema.SchemaValidateFunc { return func(v interface{}, k string) (ws []string, errors []error) { json, err := structure.NormalizeJsonString(v) diff --git a/aws/resource_aws_cloudwatch_event_rule_test.go b/aws/resource_aws_cloudwatch_event_rule_test.go index 446c0eb781a..a913a8dc6c4 100644 --- a/aws/resource_aws_cloudwatch_event_rule_test.go +++ b/aws/resource_aws_cloudwatch_event_rule_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudwatchevents" events "github.com/aws/aws-sdk-go/service/cloudwatchevents" "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -17,6 +16,7 @@ import ( "github.com/terraform-providers/terraform-provider-aws/aws/internal/naming" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents/finder" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents/lister" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func init() { @@ -95,7 +95,7 @@ func TestAccAWSCloudWatchEventRule_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, Steps: []resource.TestStep{ @@ -166,7 +166,7 @@ func TestAccAWSCloudWatchEventRule_EventBusName(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, Steps: []resource.TestStep{ @@ -216,7 +216,7 @@ func TestAccAWSCloudWatchEventRule_role(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, Steps: []resource.TestStep{ @@ -244,7 +244,7 @@ func TestAccAWSCloudWatchEventRule_description(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, Steps: []resource.TestStep{ @@ -280,7 +280,7 @@ func TestAccAWSCloudWatchEventRule_pattern(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, Steps: []resource.TestStep{ @@ -317,7 +317,7 @@ func TestAccAWSCloudWatchEventRule_ScheduleAndPattern(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, Steps: []resource.TestStep{ @@ -346,7 +346,7 @@ func TestAccAWSCloudWatchEventRule_NamePrefix(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, Steps: []resource.TestStep{ @@ -373,7 +373,7 @@ func TestAccAWSCloudWatchEventRule_Name_Generated(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, Steps: []resource.TestStep{ @@ -401,7 +401,7 @@ func TestAccAWSCloudWatchEventRule_tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, Steps: []resource.TestStep{ @@ -453,7 +453,7 @@ func TestAccAWSCloudWatchEventRule_IsEnabled(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, Steps: []resource.TestStep{ @@ -503,7 +503,7 @@ func TestAccAWSCloudWatchEventRule_PartnerEventBus(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, Steps: []resource.TestStep{ @@ -531,6 +531,42 @@ func TestAccAWSCloudWatchEventRule_PartnerEventBus(t *testing.T) { }) } +func TestAccAWSCloudWatchEventRule_EventBusArn(t *testing.T) { + var v events.DescribeRuleOutput + rName := acctest.RandomWithPrefix("tf-acc-test-rule") + resourceName := "aws_cloudwatch_event_rule.test" + eventBusName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, events.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCloudWatchEventRuleEventBusArn(rName, eventBusName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventRuleExists(resourceName, &v), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "events", regexp.MustCompile(fmt.Sprintf(`rule/%s/%s$`, eventBusName, rName))), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttrPair(resourceName, "event_bus_name", "aws_cloudwatch_event_bus.test", "arn"), + testAccCheckResourceAttrEquivalentJSON(resourceName, "event_pattern", "{\"source\":[\"aws.ec2\"]}"), + resource.TestCheckResourceAttr(resourceName, "is_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "role_arn", ""), + resource.TestCheckResourceAttr(resourceName, "schedule_expression", ""), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccCheckCloudWatchEventRuleExists(n string, rule *events.DescribeRuleOutput) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -538,15 +574,17 @@ func testAccCheckCloudWatchEventRuleExists(n string, rule *events.DescribeRuleOu return fmt.Errorf("Not found: %s", n) } + if rs.Primary.ID == "" { + return fmt.Errorf("No CloudWatch Events Rule ID is set") + } + conn := testAccProvider.Meta().(*AWSClient).cloudwatcheventsconn - resp, err := finder.RuleByID(conn, rs.Primary.ID) + resp, err := finder.RuleByResourceID(conn, rs.Primary.ID) + if err != nil { return err } - if resp == nil { - return fmt.Errorf("CloudWatch Events rule (%s) not found", rs.Primary.ID) - } *rule = *resp @@ -563,10 +601,12 @@ func testAccCheckCloudWatchEventRuleEnabled(n string, desired string) resource.T conn := testAccProvider.Meta().(*AWSClient).cloudwatcheventsconn - resp, err := finder.RuleByID(conn, rs.Primary.ID) + resp, err := finder.RuleByResourceID(conn, rs.Primary.ID) + if err != nil { return err } + if aws.StringValue(resp.State) != desired { return fmt.Errorf("Expected state %q, given %q", desired, aws.StringValue(resp.State)) } @@ -583,10 +623,17 @@ func testAccCheckAWSCloudWatchEventRuleDestroy(s *terraform.State) error { continue } - resp, err := finder.RuleByID(conn, rs.Primary.ID) - if err == nil { - return fmt.Errorf("CloudWatch Events Rule (%s) still exists: %s", rs.Primary.ID, resp) + _, err := finder.RuleByResourceID(conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue } + + if err != nil { + return err + } + + return fmt.Errorf("CloudWatch Events Rule %s still exists", rs.Primary.ID) } return nil @@ -799,3 +846,22 @@ PATTERN } `, rName, eventBusName) } + +func testAccAWSCloudWatchEventRuleEventBusArn(rName, eventBusName string) string { + return fmt.Sprintf(` +resource "aws_cloudwatch_event_bus" "test" { + name = %[2]q +} + +resource "aws_cloudwatch_event_rule" "test" { + name = %[1]q + event_bus_name = aws_cloudwatch_event_bus.test.arn + + event_pattern = <