diff --git a/aws/internal/service/cloudwatchevents/finder/finder.go b/aws/internal/service/cloudwatchevents/finder/finder.go index 76ee55a06d9..14b1829448c 100644 --- a/aws/internal/service/cloudwatchevents/finder/finder.go +++ b/aws/internal/service/cloudwatchevents/finder/finder.go @@ -1,9 +1,12 @@ package finder import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" events "github.com/aws/aws-sdk-go/service/cloudwatchevents" tfevents "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents/lister" ) func Rule(conn *events.CloudWatchEvents, eventBusName, ruleName string) (*events.DescribeRuleOutput, error) { @@ -26,3 +29,29 @@ func RuleByID(conn *events.CloudWatchEvents, ruleID string) (*events.DescribeRul return Rule(conn, busName, ruleName) } + +func Target(conn *events.CloudWatchEvents, busName, ruleName, targetId string) (*events.Target, error) { + var result *events.Target + err := lister.ListAllTargetsForRulePages(conn, busName, ruleName, func(page *events.ListTargetsByRuleOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, t := range page.Targets { + if targetId == aws.StringValue(t.Id) { + result = t + return false + } + } + + return !lastPage + }) + if err != nil { + return nil, err + } + + if result == nil { + return nil, fmt.Errorf("CloudWatch Event Target %q (\"%s/%s\") not found", targetId, busName, ruleName) + } + return result, nil +} diff --git a/aws/internal/service/cloudwatchevents/id.go b/aws/internal/service/cloudwatchevents/id.go index 80d947370ff..f7b61954eca 100644 --- a/aws/internal/service/cloudwatchevents/id.go +++ b/aws/internal/service/cloudwatchevents/id.go @@ -27,3 +27,29 @@ func RuleParseID(id string) (string, string, error) { return "", "", fmt.Errorf("unexpected format for ID (%q), expected "+ruleIDSeparator+" or ", id) } + +// 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. + +const targetIDSeparator = "-" +const targetImportIDSeparator = "/" + +func TargetCreateID(eventBusName, ruleName, targetID string) string { + id := ruleName + targetIDSeparator + targetID + if eventBusName != "" && eventBusName != DefaultEventBusName { + id = eventBusName + targetIDSeparator + id + } + 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 + } + if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" { + return parts[0], parts[1], parts[2], nil + } + + return "", "", "", fmt.Errorf("unexpected format for ID (%q), expected "+targetImportIDSeparator+""+targetImportIDSeparator+" or "+targetImportIDSeparator+"", id) +} diff --git a/aws/internal/service/cloudwatchevents/lister/list.go b/aws/internal/service/cloudwatchevents/lister/list.go index 035d01527f4..b1d3b4ecd4a 100644 --- a/aws/internal/service/cloudwatchevents/lister/list.go +++ b/aws/internal/service/cloudwatchevents/lister/list.go @@ -1,3 +1,17 @@ //go:generate go run ../../../generators/listpages/main.go -function=ListEventBuses,ListRules,ListTargetsByRule github.com/aws/aws-sdk-go/service/cloudwatchevents package lister + +import ( + "github.com/aws/aws-sdk-go/aws" + events "github.com/aws/aws-sdk-go/service/cloudwatchevents" +) + +func ListAllTargetsForRulePages(conn *events.CloudWatchEvents, busName, ruleName string, fn func(*events.ListTargetsByRuleOutput, bool) bool) error { + input := &events.ListTargetsByRuleInput{ + Rule: aws.String(ruleName), + EventBusName: aws.String(busName), + Limit: aws.Int64(100), // Set limit to allowed maximum to prevent API throttling + } + return ListTargetsByRulePages(conn, input, fn) +} diff --git a/aws/resource_aws_cloudwatch_event_rule_test.go b/aws/resource_aws_cloudwatch_event_rule_test.go index c5bb9c565c4..7e8a7fc6343 100644 --- a/aws/resource_aws_cloudwatch_event_rule_test.go +++ b/aws/resource_aws_cloudwatch_event_rule_test.go @@ -37,14 +37,14 @@ func testSweepCloudWatchEventRules(region string) error { var sweeperErrs *multierror.Error var count int - input := &events.ListRulesInput{} + rulesInput := &events.ListRulesInput{} - err = lister.ListRulesPages(conn, input, func(page *events.ListRulesOutput, lastPage bool) bool { - if page == nil { - return !lastPage + err = lister.ListRulesPages(conn, rulesInput, func(rulesPage *events.ListRulesOutput, lastRulesPage bool) bool { + if rulesPage == nil { + return !lastRulesPage } - for _, rule := range page.Rules { + for _, rule := range rulesPage.Rules { count++ name := aws.StringValue(rule.Name) @@ -59,7 +59,7 @@ func testSweepCloudWatchEventRules(region string) error { } } - return !lastPage + return !lastRulesPage }) if testSweepSkipSweepError(err) { diff --git a/aws/resource_aws_cloudwatch_event_target.go b/aws/resource_aws_cloudwatch_event_target.go index c05a372063d..bc893558bdb 100644 --- a/aws/resource_aws_cloudwatch_event_target.go +++ b/aws/resource_aws_cloudwatch_event_target.go @@ -5,14 +5,15 @@ import ( "log" "math" "regexp" - "strings" "github.com/aws/aws-sdk-go/aws" events "github.com/aws/aws-sdk-go/service/cloudwatchevents" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "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/service/cloudwatchevents/lister" + tfevents "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents/finder" ) func resourceAwsCloudWatchEventTarget() *schema.Resource { @@ -27,6 +28,14 @@ func resourceAwsCloudWatchEventTarget() *schema.Resource { }, Schema: map[string]*schema.Schema{ + "event_bus_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateCloudWatchEventBusName, + Default: tfevents.DefaultEventBusName, + }, + "rule": { Type: schema.TypeString, Required: true, @@ -51,7 +60,7 @@ func resourceAwsCloudWatchEventTarget() *schema.Resource { "input": { Type: schema.TypeString, Optional: true, - ConflictsWith: []string{"input_path"}, + ConflictsWith: []string{"input_path", "input_transformer"}, // We could be normalizing the JSON here, // but for built-in targets input may not be JSON }, @@ -59,7 +68,7 @@ func resourceAwsCloudWatchEventTarget() *schema.Resource { "input_path": { Type: schema.TypeString, Optional: true, - ConflictsWith: []string{"input"}, + ConflictsWith: []string{"input", "input_transformer"}, }, "role_arn": { @@ -207,9 +216,10 @@ func resourceAwsCloudWatchEventTarget() *schema.Resource { }, "input_transformer": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ConflictsWith: []string{"input", "input_path"}, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "input_paths": { @@ -238,31 +248,34 @@ func resourceAwsCloudWatchEventTargetCreate(d *schema.ResourceData, meta interfa rule := d.Get("rule").(string) - var targetId string + var targetID string if v, ok := d.GetOk("target_id"); ok { - targetId = v.(string) + targetID = v.(string) } else { - targetId = resource.UniqueId() - d.Set("target_id", targetId) + targetID = resource.UniqueId() + d.Set("target_id", targetID) + } + var busName string + if v, ok := d.GetOk("event_bus_name"); ok { + busName = v.(string) } input := buildPutTargetInputStruct(d) - log.Printf("[DEBUG] Creating CloudWatch Event Target: %s", input) + log.Printf("[DEBUG] Creating CloudWatch Events Target: %s", input) out, err := conn.PutTargets(input) if err != nil { - return fmt.Errorf("Creating CloudWatch Event Target failed: %s", err) + return fmt.Errorf("Creating CloudWatch Events Target failed: %w", err) } if len(out.FailedEntries) > 0 { - return fmt.Errorf("Creating CloudWatch Event Target failed: %s", - out.FailedEntries) + return fmt.Errorf("Creating CloudWatch Events Target failed: %s", out.FailedEntries) } - id := rule + "-" + targetId + id := tfevents.TargetCreateID(busName, rule, targetID) d.SetId(id) - log.Printf("[INFO] CloudWatch Event Target %q created", d.Id()) + log.Printf("[INFO] CloudWatch Events Target (%s) created", d.Id()) return resourceAwsCloudWatchEventTargetRead(d, meta) } @@ -270,27 +283,17 @@ func resourceAwsCloudWatchEventTargetCreate(d *schema.ResourceData, meta interfa func resourceAwsCloudWatchEventTargetRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cloudwatcheventsconn - t, err := findEventTargetById(conn, d.Get("target_id").(string), d.Get("rule").(string)) - if err != nil { - if regexp.MustCompile(" not found$").MatchString(err.Error()) { - log.Printf("[WARN] Removing CloudWatch Event Target %q because it's gone.", d.Id()) - d.SetId("") - return nil - } - // This should never happen, but it's useful - // for recovering from https://github.com/hashicorp/terraform/issues/5389 - if isAWSErr(err, "ValidationException", "") { - log.Printf("[WARN] Removing CloudWatch Event Target %q because it never existed.", d.Id()) - d.SetId("") - return nil - } + busName := d.Get("event_bus_name").(string) - if isAWSErr(err, events.ErrCodeResourceNotFoundException, "") { - log.Printf("[WARN] CloudWatch Event Target (%q) not found. Removing it from state.", d.Id()) + t, err := finder.Target(conn, busName, d.Get("rule").(string), d.Get("target_id").(string)) + if err != nil { + if tfawserr.ErrCodeEquals(err, "ValidationException") || + tfawserr.ErrCodeEquals(err, events.ErrCodeResourceNotFoundException) || + regexp.MustCompile(" not found$").MatchString(err.Error()) { + log.Printf("[WARN] CloudWatch Events Target (%s) not found, removing from state", d.Id()) d.SetId("") return nil } - return err } log.Printf("[DEBUG] Found Event Target: %s", t) @@ -300,86 +303,56 @@ func resourceAwsCloudWatchEventTargetRead(d *schema.ResourceData, meta interface d.Set("input", t.Input) d.Set("input_path", t.InputPath) d.Set("role_arn", t.RoleArn) + d.Set("event_bus_name", busName) if t.RunCommandParameters != nil { if err := d.Set("run_command_targets", flattenAwsCloudWatchEventTargetRunParameters(t.RunCommandParameters)); err != nil { - return fmt.Errorf("Error setting run_command_targets error: %#v", err) + return fmt.Errorf("Error setting run_command_targets error: %w", err) } } if t.EcsParameters != nil { if err := d.Set("ecs_target", flattenAwsCloudWatchEventTargetEcsParameters(t.EcsParameters)); err != nil { - return fmt.Errorf("Error setting ecs_target error: %#v", err) + return fmt.Errorf("Error setting ecs_target error: %w", err) } } if t.BatchParameters != nil { if err := d.Set("batch_target", flattenAwsCloudWatchEventTargetBatchParameters(t.BatchParameters)); err != nil { - return fmt.Errorf("Error setting batch_target error: %#v", err) + return fmt.Errorf("Error setting batch_target error: %w", err) } } if t.KinesisParameters != nil { if err := d.Set("kinesis_target", flattenAwsCloudWatchEventTargetKinesisParameters(t.KinesisParameters)); err != nil { - return fmt.Errorf("Error setting kinesis_target error: %#v", err) + return fmt.Errorf("Error setting kinesis_target error: %w", err) } } if t.SqsParameters != nil { if err := d.Set("sqs_target", flattenAwsCloudWatchEventTargetSqsParameters(t.SqsParameters)); err != nil { - return fmt.Errorf("Error setting sqs_target error: %#v", err) + return fmt.Errorf("Error setting sqs_target error: %w", err) } } if t.InputTransformer != nil { if err := d.Set("input_transformer", flattenAwsCloudWatchInputTransformer(t.InputTransformer)); err != nil { - return fmt.Errorf("Error setting input_transformer error: %#v", err) + return fmt.Errorf("Error setting input_transformer error: %w", err) } } return nil } -func findEventTargetById(conn *events.CloudWatchEvents, id, rule string) (*events.Target, error) { - input := &events.ListTargetsByRuleInput{ - Rule: aws.String(rule), - Limit: aws.Int64(100), // Set limit to allowed maximum to prevent API throttling - } - var result *events.Target - - err := lister.ListTargetsByRulePages(conn, input, func(page *events.ListTargetsByRuleOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, t := range page.Targets { - if id == aws.StringValue(t.Id) { - result = t - return false - } - } - - return !lastPage - }) - if err != nil { - return nil, err - } - - if result == nil { - return nil, fmt.Errorf("CloudWatch Event Target %q (%q) not found", id, rule) - } - return result, nil -} - func resourceAwsCloudWatchEventTargetUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cloudwatcheventsconn input := buildPutTargetInputStruct(d) - log.Printf("[DEBUG] Updating CloudWatch Event Target: %s", input) + log.Printf("[DEBUG] Updating CloudWatch Events Target: %s", input) _, err := conn.PutTargets(input) if err != nil { - return fmt.Errorf("Updating CloudWatch Event Target failed: %s", err) + return fmt.Errorf("error updating CloudWatch Events Target (%s): %w", d.Id(), err) } return resourceAwsCloudWatchEventTargetRead(d, meta) @@ -388,20 +361,24 @@ func resourceAwsCloudWatchEventTargetUpdate(d *schema.ResourceData, meta interfa func resourceAwsCloudWatchEventTargetDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cloudwatcheventsconn + busName := d.Get("event_bus_name").(string) input := &events.RemoveTargetsInput{ - Ids: []*string{aws.String(d.Get("target_id").(string))}, - Rule: aws.String(d.Get("rule").(string)), + Ids: []*string{aws.String(d.Get("target_id").(string))}, + Rule: aws.String(d.Get("rule").(string)), + EventBusName: aws.String(busName), } output, err := conn.RemoveTargets(input) - if err != nil { - return fmt.Errorf("error deleting CloudWatch Event Target (%s): %s", d.Id(), err) + if tfawserr.ErrCodeEquals(err, events.ErrCodeResourceNotFoundException) { + return nil + } + return fmt.Errorf("error deleting CloudWatch Events Target (%s): %w", d.Id(), err) } if output != nil && len(output.FailedEntries) > 0 && output.FailedEntries[0] != nil { failedEntry := output.FailedEntries[0] - return fmt.Errorf("error deleting CloudWatch Event Target (%s): failure entry: %s: %s", d.Id(), aws.StringValue(failedEntry.ErrorCode), aws.StringValue(failedEntry.ErrorMessage)) + return fmt.Errorf("error deleting CloudWatch Events Target (%s): failure entry: %s: %s", d.Id(), aws.StringValue(failedEntry.ErrorCode), aws.StringValue(failedEntry.ErrorMessage)) } return nil @@ -450,21 +427,21 @@ func buildPutTargetInputStruct(d *schema.ResourceData) *events.PutTargetsInput { Rule: aws.String(d.Get("rule").(string)), Targets: []*events.Target{e}, } + if v, ok := d.GetOk("event_bus_name"); ok { + input.EventBusName = aws.String(v.(string)) + } return &input } func expandAwsCloudWatchEventTargetRunParameters(config []interface{}) *events.RunCommandParameters { - commands := make([]*events.RunCommandTarget, 0) - for _, c := range config { param := c.(map[string]interface{}) command := &events.RunCommandTarget{ Key: aws.String(param["key"].(string)), Values: expandStringList(param["values"].([]interface{})), } - commands = append(commands, command) } @@ -612,6 +589,7 @@ func flattenAwsCloudWatchEventTargetEcsParameters(ecsParameters *events.EcsParam result := []map[string]interface{}{config} return result } + func flattenAwsCloudWatchEventTargetEcsParametersNetworkConfiguration(nc *events.NetworkConfiguration) []interface{} { if nc == nil { return nil @@ -670,17 +648,16 @@ func flattenAwsCloudWatchInputTransformer(inputTransformer *events.InputTransfor } func resourceAwsCloudWatchEventTargetImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - idParts := strings.SplitN(d.Id(), "/", 2) - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - return nil, fmt.Errorf("unexpected format (%q), expected /", d.Id()) + busName, ruleName, targetID, err := tfevents.TargetParseImportID(d.Id()) + if err != nil { + return []*schema.ResourceData{}, err } - ruleName := idParts[0] - targetName := idParts[1] - - d.Set("target_id", targetName) + id := tfevents.TargetCreateID(busName, ruleName, targetID) + d.SetId(id) + d.Set("target_id", targetID) d.Set("rule", ruleName) - d.SetId(ruleName + "-" + targetName) + d.Set("event_bus_name", busName) return []*schema.ResourceData{d}, nil } diff --git a/aws/resource_aws_cloudwatch_event_target_test.go b/aws/resource_aws_cloudwatch_event_target_test.go index 04fc8d81afa..c6d3e17a84e 100644 --- a/aws/resource_aws_cloudwatch_event_target_test.go +++ b/aws/resource_aws_cloudwatch_event_target_test.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "regexp" + "strconv" "strings" "testing" @@ -13,6 +14,8 @@ import ( "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/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" ) @@ -26,7 +29,7 @@ func init() { func testSweepCloudWatchEventTargets(region string) error { client, err := sharedClientForRegion(region) if err != nil { - return fmt.Errorf("Error getting client: %s", err) + return fmt.Errorf("Error getting client: %w", err) } conn := client.(*AWSClient).cloudwatcheventsconn @@ -104,14 +107,14 @@ func testSweepCloudWatchEventTargets(region string) error { func TestAccAWSCloudWatchEventTarget_basic(t *testing.T) { resourceName := "aws_cloudwatch_event_target.test" + snsTopicResourceName := "aws_sns_topic.test" - var target events.Target - topicResourceName := "aws_sns_topic.test" - ruleName := acctest.RandomWithPrefix("tf-acc-cw-event-rule-basic") - snsTopicName1 := acctest.RandomWithPrefix("tf-acc-topic") - snsTopicName2 := acctest.RandomWithPrefix("tf-acc-topic-second") - targetID1 := acctest.RandomWithPrefix("tf-acc-cw-target") - targetID2 := acctest.RandomWithPrefix("tf-acc-cw-target-second") + var v1, v2 events.Target + ruleName := acctest.RandomWithPrefix("tf-acc-test-rule") + snsTopicName1 := acctest.RandomWithPrefix("tf-acc-test-sns") + snsTopicName2 := acctest.RandomWithPrefix("tf-acc-test-sns") + targetID1 := acctest.RandomWithPrefix("tf-acc-test-target") + targetID2 := acctest.RandomWithPrefix("tf-acc-test-target") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -121,19 +124,76 @@ func TestAccAWSCloudWatchEventTarget_basic(t *testing.T) { { Config: testAccAWSCloudWatchEventTargetConfig(ruleName, snsTopicName1, targetID1), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudWatchEventTargetExists(resourceName, &target), + testAccCheckCloudWatchEventTargetExists(resourceName, &v1), resource.TestCheckResourceAttr(resourceName, "rule", ruleName), + resource.TestCheckResourceAttr(resourceName, "event_bus_name", "default"), resource.TestCheckResourceAttr(resourceName, "target_id", targetID1), - resource.TestCheckResourceAttrPair(resourceName, "arn", topicResourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "arn", snsTopicResourceName, "arn"), + + resource.TestCheckResourceAttr(resourceName, "input", ""), + resource.TestCheckResourceAttr(resourceName, "input_path", ""), + resource.TestCheckResourceAttr(resourceName, "role_arn", ""), + resource.TestCheckResourceAttr(resourceName, "run_command_targets.#", "0"), + resource.TestCheckResourceAttr(resourceName, "ecs_target.#", "0"), + resource.TestCheckResourceAttr(resourceName, "batch_target.#", "0"), + resource.TestCheckResourceAttr(resourceName, "kinesis_target.#", "0"), + resource.TestCheckResourceAttr(resourceName, "sqs_target.#", "0"), + resource.TestCheckResourceAttr(resourceName, "input_transformer.#", "0"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAWSCloudWatchEventTargetImportStateIdFunc(resourceName), + ImportStateVerify: true, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAWSCloudWatchEventTargetNoBusNameImportStateIdFunc(resourceName), + ImportStateVerify: true, + }, { Config: testAccAWSCloudWatchEventTargetConfig(ruleName, snsTopicName2, targetID2), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudWatchEventTargetExists(resourceName, &target), + testAccCheckCloudWatchEventTargetExists(resourceName, &v2), resource.TestCheckResourceAttr(resourceName, "rule", ruleName), + resource.TestCheckResourceAttr(resourceName, "event_bus_name", "default"), resource.TestCheckResourceAttr(resourceName, "target_id", targetID2), - resource.TestCheckResourceAttrPair(resourceName, "arn", topicResourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "arn", snsTopicResourceName, "arn"), + ), + }, + { + Config: testAccAWSCloudWatchEventTargetConfigDefaultEventBusName(ruleName, snsTopicName2, targetID2), + PlanOnly: true, + }, + }, + }) +} + +func TestAccAWSCloudWatchEventTarget_EventBusName(t *testing.T) { + resourceName := "aws_cloudwatch_event_target.test" + + var v1, v2 events.Target + ruleName := acctest.RandomWithPrefix("tf-acc-test-rule") + busName := acctest.RandomWithPrefix("tf-acc-test-bus") + snsTopicName1 := acctest.RandomWithPrefix("tf-acc-test-sns") + snsTopicName2 := acctest.RandomWithPrefix("tf-acc-test-sns") + targetID1 := acctest.RandomWithPrefix("tf-acc-test-target") + targetID2 := acctest.RandomWithPrefix("tf-acc-test-target") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudWatchEventTargetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCloudWatchEventTargetConfigEventBusName(ruleName, busName, snsTopicName1, targetID1), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventTargetExists(resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, "rule", ruleName), + resource.TestCheckResourceAttr(resourceName, "event_bus_name", busName), + resource.TestCheckResourceAttr(resourceName, "target_id", targetID1), ), }, { @@ -142,15 +202,24 @@ func TestAccAWSCloudWatchEventTarget_basic(t *testing.T) { ImportStateIdFunc: testAccAWSCloudWatchEventTargetImportStateIdFunc(resourceName), ImportStateVerify: true, }, + { + Config: testAccAWSCloudWatchEventTargetConfigEventBusName(ruleName, busName, snsTopicName2, targetID2), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventTargetExists(resourceName, &v2), + resource.TestCheckResourceAttr(resourceName, "rule", ruleName), + resource.TestCheckResourceAttr(resourceName, "event_bus_name", busName), + resource.TestCheckResourceAttr(resourceName, "target_id", targetID2), + ), + }, }, }) } -func TestAccAWSCloudWatchEventTarget_missingTargetId(t *testing.T) { +func TestAccAWSCloudWatchEventTarget_GeneratedTargetId(t *testing.T) { resourceName := "aws_cloudwatch_event_target.test" + snsTopicResourceName := "aws_sns_topic.test" - var target events.Target - topicResourceName := "aws_sns_topic.test" + var v events.Target ruleName := acctest.RandomWithPrefix("tf-acc-cw-event-rule-missing-target-id") snsTopicName := acctest.RandomWithPrefix("tf-acc") @@ -162,9 +231,10 @@ func TestAccAWSCloudWatchEventTarget_missingTargetId(t *testing.T) { { Config: testAccAWSCloudWatchEventTargetConfigMissingTargetId(ruleName, snsTopicName), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudWatchEventTargetExists(resourceName, &target), + testAccCheckCloudWatchEventTargetExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "rule", ruleName), - resource.TestCheckResourceAttrPair(resourceName, "arn", topicResourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "arn", snsTopicResourceName, "arn"), + naming.TestCheckResourceAttrNameGenerated(resourceName, "target_id"), ), }, { @@ -179,8 +249,9 @@ func TestAccAWSCloudWatchEventTarget_missingTargetId(t *testing.T) { func TestAccAWSCloudWatchEventTarget_full(t *testing.T) { resourceName := "aws_cloudwatch_event_target.test" - var target events.Target - streamResourceName := "aws_kinesis_stream.test" + kinesisStreamResourceName := "aws_kinesis_stream.test" + var v events.Target + ruleName := acctest.RandomWithPrefix("tf-acc-cw-event-rule-full") ssmDocumentName := acctest.RandomWithPrefix("tf_ssm_Document") targetID := acctest.RandomWithPrefix("tf-acc-cw-target-full") @@ -193,11 +264,11 @@ func TestAccAWSCloudWatchEventTarget_full(t *testing.T) { { Config: testAccAWSCloudWatchEventTargetConfig_full(ruleName, targetID, ssmDocumentName), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudWatchEventTargetExists(resourceName, &target), + testAccCheckCloudWatchEventTargetExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "rule", ruleName), resource.TestCheckResourceAttr(resourceName, "target_id", targetID), - resource.TestCheckResourceAttrPair(resourceName, "arn", streamResourceName, "arn"), - resource.TestCheckResourceAttr(resourceName, "input", "{ \"source\": [\"aws.cloudtrail\"] }\n"), + resource.TestCheckResourceAttrPair(resourceName, "arn", kinesisStreamResourceName, "arn"), + testAccCheckResourceAttrEquivalentJSON(resourceName, "input", `{"source": ["aws.cloudtrail"]}`), resource.TestCheckResourceAttr(resourceName, "input_path", ""), ), }, @@ -211,9 +282,35 @@ func TestAccAWSCloudWatchEventTarget_full(t *testing.T) { }) } +func TestAccAWSCloudWatchEventTarget_disappears(t *testing.T) { + var v events.Target + + ruleName := acctest.RandomWithPrefix("tf-acc-test") + snsTopicName := acctest.RandomWithPrefix("tf-acc-test-sns") + targetID := acctest.RandomWithPrefix("tf-acc-test-target") + + resourceName := "aws_cloudwatch_event_target.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudWatchEventTargetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCloudWatchEventTargetConfig(ruleName, snsTopicName, targetID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventTargetExists(resourceName, &v), + testAccCheckResourceDisappears(testAccProvider, resourceAwsCloudWatchEventTarget(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func TestAccAWSCloudWatchEventTarget_ssmDocument(t *testing.T) { - var target events.Target resourceName := "aws_cloudwatch_event_target.test" + var v events.Target rName := acctest.RandomWithPrefix("tf_ssm_Document") resource.ParallelTest(t, resource.TestCase{ @@ -224,7 +321,11 @@ func TestAccAWSCloudWatchEventTarget_ssmDocument(t *testing.T) { { Config: testAccAWSCloudWatchEventTargetConfigSsmDocument(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudWatchEventTargetExists(resourceName, &target), + testAccCheckCloudWatchEventTargetExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "run_command_targets.#", "1"), + resource.TestCheckResourceAttr(resourceName, "run_command_targets.0.key", "tag:Name"), + resource.TestCheckResourceAttr(resourceName, "run_command_targets.0.values.#", "1"), + resource.TestCheckResourceAttr(resourceName, "run_command_targets.0.values.0", "acceptance_test"), ), }, { @@ -238,8 +339,10 @@ func TestAccAWSCloudWatchEventTarget_ssmDocument(t *testing.T) { } func TestAccAWSCloudWatchEventTarget_ecs(t *testing.T) { - var target events.Target resourceName := "aws_cloudwatch_event_target.test" + iamRoleResourceName := "aws_iam_role.test" + ecsTaskDefinitionResourceName := "aws_ecs_task_definition.task" + var v events.Target rName := acctest.RandomWithPrefix("tf_ecs_target") resource.ParallelTest(t, resource.TestCase{ @@ -250,7 +353,14 @@ func TestAccAWSCloudWatchEventTarget_ecs(t *testing.T) { { Config: testAccAWSCloudWatchEventTargetConfigEcs(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudWatchEventTargetExists(resourceName, &target), + testAccCheckCloudWatchEventTargetExists(resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, "role_arn", iamRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "ecs_target.#", "1"), + resource.TestCheckResourceAttr(resourceName, "ecs_target.0.task_count", "1"), + resource.TestCheckResourceAttrPair(resourceName, "ecs_target.0.task_definition_arn", ecsTaskDefinitionResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "ecs_target.0.launch_type", "FARGATE"), + resource.TestCheckResourceAttr(resourceName, "ecs_target.0.network_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "ecs_target.0.network_configuration.0.subnets.#", "1"), ), }, { @@ -264,8 +374,8 @@ func TestAccAWSCloudWatchEventTarget_ecs(t *testing.T) { } func TestAccAWSCloudWatchEventTarget_ecsWithBlankTaskCount(t *testing.T) { - var target events.Target resourceName := "aws_cloudwatch_event_target.test" + var v events.Target rName := acctest.RandomWithPrefix("tf_ecs_target") resource.ParallelTest(t, resource.TestCase{ @@ -276,7 +386,8 @@ func TestAccAWSCloudWatchEventTarget_ecsWithBlankTaskCount(t *testing.T) { { Config: testAccAWSCloudWatchEventTargetConfigEcsWithBlankTaskCount(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudWatchEventTargetExists(resourceName, &target), + testAccCheckCloudWatchEventTargetExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "ecs_target.#", "1"), resource.TestCheckResourceAttr(resourceName, "ecs_target.0.task_count", "1"), ), }, @@ -291,8 +402,9 @@ func TestAccAWSCloudWatchEventTarget_ecsWithBlankTaskCount(t *testing.T) { } func TestAccAWSCloudWatchEventTarget_batch(t *testing.T) { - var target events.Target resourceName := "aws_cloudwatch_event_target.test" + batchJobDefinitionResourceName := "aws_batch_job_definition.test" + var v events.Target rName := acctest.RandomWithPrefix("tf_batch_target") resource.ParallelTest(t, resource.TestCase{ @@ -303,7 +415,10 @@ func TestAccAWSCloudWatchEventTarget_batch(t *testing.T) { { Config: testAccAWSCloudWatchEventTargetConfigBatch(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudWatchEventTargetExists(resourceName, &target), + testAccCheckCloudWatchEventTargetExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "batch_target.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "batch_target.0.job_definition", batchJobDefinitionResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "batch_target.0.job_name", rName), ), }, { @@ -317,8 +432,8 @@ func TestAccAWSCloudWatchEventTarget_batch(t *testing.T) { } func TestAccAWSCloudWatchEventTarget_kinesis(t *testing.T) { - var target events.Target resourceName := "aws_cloudwatch_event_target.test" + var v events.Target rName := acctest.RandomWithPrefix("tf_kinesis_target") resource.ParallelTest(t, resource.TestCase{ @@ -329,22 +444,23 @@ func TestAccAWSCloudWatchEventTarget_kinesis(t *testing.T) { { Config: testAccAWSCloudWatchEventTargetConfigKinesis(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudWatchEventTargetExists(resourceName, &target), + testAccCheckCloudWatchEventTargetExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "kinesis_target.#", "1"), + resource.TestCheckResourceAttr(resourceName, "kinesis_target.0.partition_key_path", "$.detail"), ), }, { ResourceName: resourceName, ImportState: true, - ImportStateIdFunc: testAccAWSCloudWatchEventTargetImportStateIdFunc(resourceName), - ImportStateVerify: true, + ImportStateIdFunc: testAccAWSCloudWatchEventTargetImportStateIdFunc(resourceName), ImportStateVerify: true, }, }, }) } func TestAccAWSCloudWatchEventTarget_sqs(t *testing.T) { - var target events.Target resourceName := "aws_cloudwatch_event_target.test" + var v events.Target rName := acctest.RandomWithPrefix("tf_sqs_target") resource.ParallelTest(t, resource.TestCase{ @@ -355,7 +471,9 @@ func TestAccAWSCloudWatchEventTarget_sqs(t *testing.T) { { Config: testAccAWSCloudWatchEventTargetConfigSqs(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudWatchEventTargetExists(resourceName, &target), + testAccCheckCloudWatchEventTargetExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "sqs_target.#", "1"), + resource.TestCheckResourceAttr(resourceName, "sqs_target.0.message_group_id", "event_group"), ), }, { @@ -369,23 +487,64 @@ func TestAccAWSCloudWatchEventTarget_sqs(t *testing.T) { } func TestAccAWSCloudWatchEventTarget_input_transformer(t *testing.T) { - var target events.Target resourceName := "aws_cloudwatch_event_target.test" + var v events.Target rName := acctest.RandomWithPrefix("tf_input_transformer") + tooManyInputPaths := []string{ + "account", + "count", + "eventFirstSeen", + "eventLastSeen", + "Finding_ID", + "Finding_Type", + "instanceId", + "port", + "region", + "severity", + "time", + } + validInputPaths := []string{ + "account", + "count", + "eventFirstSeen", + "eventLastSeen", + "Finding_ID", + "Finding_Type", + "instanceId", + "region", + "severity", + "time", + } + var expectedInputTemplate strings.Builder + fmt.Fprintf(&expectedInputTemplate, `{ + "detail-type": "Scheduled Event", + "source": "aws.events", +`) + for _, path := range validInputPaths { + fmt.Fprintf(&expectedInputTemplate, " \"%[1]s\": <%[1]s>,\n", path) + } + fmt.Fprintf(&expectedInputTemplate, ` "detail": {} +} +`) + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudWatchEventTargetDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSCloudWatchEventTargetConfigInputTransformer(rName, 11), - ExpectError: regexp.MustCompile(`.*expected number of items in.* to be lesser than or equal to.*`), + Config: testAccAWSCloudWatchEventTargetConfigInputTransformer(rName, tooManyInputPaths), + ExpectError: regexp.MustCompile(`.*expected number of items in.* to be less than or equal to.*`), }, { - Config: testAccAWSCloudWatchEventTargetConfigInputTransformer(rName, 10), + Config: testAccAWSCloudWatchEventTargetConfigInputTransformer(rName, validInputPaths), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudWatchEventTargetExists(resourceName, &target), + testAccCheckCloudWatchEventTargetExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "input_transformer.#", "1"), + resource.TestCheckResourceAttr(resourceName, "input_transformer.0.input_paths.%", strconv.Itoa(len(validInputPaths))), + resource.TestCheckResourceAttr(resourceName, "input_transformer.0.input_paths.time", "$.time"), + resource.TestCheckResourceAttr(resourceName, "input_transformer.0.input_template", expectedInputTemplate.String()), ), }, { @@ -398,29 +557,6 @@ func TestAccAWSCloudWatchEventTarget_input_transformer(t *testing.T) { }) } -func TestAccAWSCloudWatchEventTarget_disappears(t *testing.T) { - resourceName := "aws_cloudwatch_event_target.test" - - var target events.Target - ruleName := acctest.RandomWithPrefix("tf-acc-cw-event-rule-basic") - snsTopicName1 := acctest.RandomWithPrefix("tf-acc-topic") - targetID1 := acctest.RandomWithPrefix("tf-acc-cw-target") - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSCloudWatchEventTargetDestroy, - Steps: []resource.TestStep{ - { - Config: testAccAWSCloudWatchEventTargetConfig(ruleName, snsTopicName1, targetID1), - Check: resource.ComposeTestCheckFunc( - testAccCheckCloudWatchEventTargetExists(resourceName, &target), - ), - }, - }, - }) -} - func TestAccAWSCloudWatchEventTarget_inputTransformerJsonString(t *testing.T) { var target events.Target rName := acctest.RandomWithPrefix("tf-acc-test") @@ -454,9 +590,9 @@ func testAccCheckCloudWatchEventTargetExists(n string, rule *events.Target) reso } conn := testAccProvider.Meta().(*AWSClient).cloudwatcheventsconn - t, err := findEventTargetById(conn, rs.Primary.Attributes["target_id"], rs.Primary.Attributes["rule"]) + t, err := finder.Target(conn, rs.Primary.Attributes["event_bus_name"], rs.Primary.Attributes["rule"], rs.Primary.Attributes["target_id"]) if err != nil { - return fmt.Errorf("Event Target not found: %s", err) + return fmt.Errorf("Event Target not found: %w", err) } *rule = *t @@ -473,7 +609,7 @@ func testAccCheckAWSCloudWatchEventTargetDestroy(s *terraform.State) error { continue } - t, err := findEventTargetById(conn, rs.Primary.Attributes["target_id"], rs.Primary.Attributes["rule"]) + t, err := finder.Target(conn, rs.Primary.Attributes["event_bus_name"], rs.Primary.Attributes["rule"], rs.Primary.Attributes["target_id"]) if err == nil { return fmt.Errorf("CloudWatch Events Target %q still exists: %s", rs.Primary.ID, t) @@ -484,6 +620,17 @@ func testAccCheckAWSCloudWatchEventTargetDestroy(s *terraform.State) error { } func testAccAWSCloudWatchEventTargetImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return fmt.Sprintf("%s/%s/%s", rs.Primary.Attributes["event_bus_name"], rs.Primary.Attributes["rule"], rs.Primary.Attributes["target_id"]), nil + } +} + +func testAccAWSCloudWatchEventTargetNoBusNameImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { return func(s *terraform.State) (string, error) { rs, ok := s.RootModule().Resources[resourceName] if !ok { @@ -513,6 +660,58 @@ resource "aws_sns_topic" "test" { `, ruleName, targetID, snsTopicName) } +func testAccAWSCloudWatchEventTargetConfigDefaultEventBusName(ruleName, snsTopicName, targetID string) string { + return fmt.Sprintf(` +resource "aws_cloudwatch_event_rule" "test" { + name = "%s" + event_bus_name = "default" + schedule_expression = "rate(1 hour)" +} + +resource "aws_cloudwatch_event_target" "test" { + rule = aws_cloudwatch_event_rule.test.name + event_bus_name = aws_cloudwatch_event_rule.test.event_bus_name + target_id = "%s" + arn = aws_sns_topic.test.arn +} + +resource "aws_sns_topic" "test" { + name = "%s" +} +`, ruleName, targetID, snsTopicName) +} + +func testAccAWSCloudWatchEventTargetConfigEventBusName(ruleName, eventBusName, snsTopicName, targetID string) string { + return fmt.Sprintf(` +resource "aws_cloudwatch_event_target" "test" { + rule = aws_cloudwatch_event_rule.test.name + event_bus_name = aws_cloudwatch_event_rule.test.event_bus_name + target_id = %[1]q + arn = aws_sns_topic.test.arn +} + +resource "aws_sns_topic" "test" { + name = %[2]q +} + +resource "aws_cloudwatch_event_rule" "test" { + name = %[3]q + event_bus_name = aws_cloudwatch_event_bus.test.name + event_pattern = <,`, sampleInputPaths[i], sampleInputPaths[i]) + for _, inputPath := range inputPathKeys { + fmt.Fprintf(&inputPaths, " %[1]s = \"$.%[1]s\"\n", inputPath) + fmt.Fprintf(&inputTemplates, " \"%[1]s\": <%[1]s>,\n", inputPath) } return composeConfig( @@ -1188,7 +1364,8 @@ resource "aws_cloudwatch_event_target" "test" { input_template = < max { - errors = append(errors, fmt.Errorf("expected number of items in %s to be lesser than or equal to %d, got %d", k, max, len(m))) + errors = append(errors, fmt.Errorf("expected number of items in %s to be less than or equal to %d, got %d", k, max, len(m))) } return warnings, errors diff --git a/website/docs/r/cloudwatch_event_target.html.markdown b/website/docs/r/cloudwatch_event_target.html.markdown index a15b5ea3f13..6a84dd0598c 100644 --- a/website/docs/r/cloudwatch_event_target.html.markdown +++ b/website/docs/r/cloudwatch_event_target.html.markdown @@ -286,8 +286,6 @@ resource "aws_cloudwatch_event_rule" "example" { ## Argument Reference --> **Note:** `input` and `input_path` are mutually exclusive options. - -> **Note:** In order to be able to have your AWS Lambda function or SNS topic invoked by an EventBridge rule, you must setup the right permissions using [`aws_lambda_permission`](https://www.terraform.io/docs/providers/aws/r/lambda_permission.html) @@ -297,18 +295,19 @@ resource "aws_cloudwatch_event_rule" "example" { The following arguments are supported: * `rule` - (Required) The name of the rule you want to add targets to. +* `event_bus_name` - (Optional) The event bus to associate with the rule. If you omit this, the `default` event bus is used. * `target_id` - (Optional) The unique target assignment ID. If missing, will generate a random, unique id. * `arn` - (Required) The Amazon Resource Name (ARN) associated of the target. -* `input` - (Optional) Valid JSON text passed to the target. +* `input` - (Optional) Valid JSON text passed to the target. Conflicts with `input_path` and `input_transformer`. * `input_path` - (Optional) The value of the [JSONPath](http://goessner.net/articles/JsonPath/) - that is used for extracting part of the matched event when passing it to the target. + that is used for extracting part of the matched event when passing it to the target. Conflicts with `input` and `input_transformer`. * `role_arn` - (Optional) The Amazon Resource Name (ARN) of the IAM role to be used for this target when the rule is triggered. Required if `ecs_target` is used. * `run_command_targets` - (Optional) Parameters used when you are using the rule to invoke Amazon EC2 Run Command. Documented below. A maximum of 5 are allowed. * `ecs_target` - (Optional) Parameters used when you are using the rule to invoke Amazon ECS Task. Documented below. A maximum of 1 are allowed. * `batch_target` - (Optional) Parameters used when you are using the rule to invoke an Amazon Batch Job. Documented below. A maximum of 1 are allowed. * `kinesis_target` - (Optional) Parameters used when you are using the rule to invoke an Amazon Kinesis Stream. Documented below. A maximum of 1 are allowed. * `sqs_target` - (Optional) Parameters used when you are using the rule to invoke an Amazon SQS Queue. Documented below. A maximum of 1 are allowed. -* `input_transformer` - (Optional) Parameters used when you are providing a custom input to a target based on certain event data. +* `input_transformer` - (Optional) Parameters used when you are providing a custom input to a target based on certain event data. Conflicts with `input` and `input_path`. `run_command_targets` support the following: @@ -358,7 +357,7 @@ For more information, see [Task Networking](https://docs.aws.amazon.com/AmazonEC ## Import -EventBridge Targets can be imported using the role event_rule and target_id separated by `/`. +EventBridge Targets can be imported using `event_bus_name/rule-name/target-id` (if you omit `event_bus_name`, the `default` event bus will be used). ``` $ terraform import aws_cloudwatch_event_target.test-event-target rule-name/target-id