Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add eventbus to cloudwatch event rule #15727

Merged
merged 14 commits into from
Oct 27, 2020
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions aws/internal/service/cloudwatchevents/finder/finder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package finder

import (
"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"
)

func Rule(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)

}

func RuleByID(conn *events.CloudWatchEvents, ruleID string) (*events.DescribeRuleOutput, error) {
busName, ruleName, err := tfevents.RuleParseID(ruleID)
if err != nil {
return nil, err
}

return Rule(conn, busName, ruleName)
}
29 changes: 29 additions & 0 deletions aws/internal/service/cloudwatchevents/id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cloudwatchevents

import (
"fmt"
"strings"
)

const DefaultEventBusName = "default"

const ruleIDSeparator = "/"

func RuleCreateID(eventBusName, ruleName string) string {
if eventBusName == "" || eventBusName == DefaultEventBusName {
return ruleName
}
return eventBusName + ruleIDSeparator + ruleName
}

func RuleParseID(id string) (string, string, error) {
parts := strings.Split(id, ruleIDSeparator)
if len(parts) == 1 && parts[0] != "" {
return DefaultEventBusName, parts[0], nil
}
if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
return parts[0], parts[1], nil
}

return "", "", fmt.Errorf("unexpected format for ID (%q), expected <event-bus-name>"+ruleIDSeparator+"<rule-name> or <rule-name>", id)
}
139 changes: 81 additions & 58 deletions aws/resource_aws_cloudwatch_event_rule.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package aws

import (
"errors"
"fmt"
"log"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
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/structure"
"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/naming"
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"
)

const (
Expand All @@ -39,20 +44,30 @@ func resourceAwsCloudWatchEventRule() *schema.Resource {
ValidateFunc: validateCloudWatchEventRuleName,
},
"name_prefix": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validateCloudWatchEventRuleName,
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"name"},
ValidateFunc: validateCloudWatchEventRuleName,
},
"schedule_expression": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringLenBetween(0, 256),
AtLeastOneOf: []string{"schedule_expression", "event_pattern"},
},
"event_bus_name": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validateCloudWatchEventBusName,
Default: tfevents.DefaultEventBusName,
},
"event_pattern": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateEventPatternValue(),
AtLeastOneOf: []string{"schedule_expression", "event_pattern"},
StateFunc: func(v interface{}) string {
json, _ := structure.NormalizeJsonString(v.(string))
return json
Expand Down Expand Up @@ -85,28 +100,26 @@ func resourceAwsCloudWatchEventRule() *schema.Resource {
func resourceAwsCloudWatchEventRuleCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cloudwatcheventsconn

var name string
if v, ok := d.GetOk("name"); ok {
name = v.(string)
} else if v, ok := d.GetOk("name_prefix"); ok {
name = resource.PrefixedUniqueId(v.(string))
} else {
name = resource.UniqueId()
}
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 Event Rule failed: %s", err)
return fmt.Errorf("Creating CloudWatch Events Rule failed: %w", err)
}
log.Printf("[DEBUG] Creating CloudWatch Event Rule: %s", input)

if v, ok := d.GetOk("tags"); ok {
input.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().CloudwatcheventsTags()
}

log.Printf("[DEBUG] Creating CloudWatch Events Rule: %s", input)

// IAM Roles take some time to propagate
var out *events.PutRuleOutput
err = resource.Retry(30*time.Second, func() *resource.RetryError {
err = resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError {
out, err = conn.PutRule(input)

if isAWSErr(err, "ValidationException", "cannot be assumed by principal") {
log.Printf("[DEBUG] Retrying update of CloudWatch Event Rule %q", *input.Name)
log.Printf("[DEBUG] Retrying update of CloudWatch Events Rule %q", aws.StringValue(input.Name))
return resource.RetryableError(err)
}
if err != nil {
Expand All @@ -119,13 +132,15 @@ func resourceAwsCloudWatchEventRuleCreate(d *schema.ResourceData, meta interface
}

if err != nil {
return fmt.Errorf("Updating CloudWatch Event Rule failed: %s", err)
return fmt.Errorf("Creating CloudWatch Events Rule failed: %w", err)
}

d.Set("arn", out.RuleArn)
d.SetId(*input.Name)

log.Printf("[INFO] CloudWatch Event Rule %q created", *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))

return resourceAwsCloudWatchEventRuleRead(d, meta)
}
Expand All @@ -134,36 +149,32 @@ func resourceAwsCloudWatchEventRuleRead(d *schema.ResourceData, meta interface{}
conn := meta.(*AWSClient).cloudwatcheventsconn
ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig

input := events.DescribeRuleInput{
Name: aws.String(d.Id()),
}
log.Printf("[DEBUG] Reading CloudWatch Event Rule: %s", input)
out, err := conn.DescribeRule(&input)
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == events.ErrCodeResourceNotFoundException {
log.Printf("[WARN] Removing CloudWatch Event Rule %q because it's gone.", d.Id())
d.SetId("")
return nil
}
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())
d.SetId("")
return nil
}
if err != nil {
return err
return fmt.Errorf("error reading CloudWatch Events Rule (%s): %w", d.Id(), err)
}
log.Printf("[DEBUG] Found Event Rule: %s", out)

arn := *out.Arn
arn := aws.StringValue(out.Arn)
d.Set("arn", arn)
d.Set("description", out.Description)
if out.EventPattern != nil {
pattern, err := structure.NormalizeJsonString(*out.EventPattern)
pattern, err := structure.NormalizeJsonString(aws.StringValue(out.EventPattern))
if err != nil {
return fmt.Errorf("event pattern contains an invalid JSON: %s", err)
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", aws.StringValue(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)

boolState, err := getBooleanStateFromString(*out.State)
if err != nil {
Expand All @@ -175,31 +186,34 @@ func resourceAwsCloudWatchEventRuleRead(d *schema.ResourceData, meta interface{}
tags, err := keyvaluetags.CloudwatcheventsListTags(conn, arn)

if err != nil {
return fmt.Errorf("error listing tags for CloudWatch Event Rule (%s): %s", arn, err)
return fmt.Errorf("error listing tags for CloudWatch Events Rule (%s): %w", arn, err)
}

if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil {
return fmt.Errorf("error setting tags: %s", err)
return fmt.Errorf("error setting tags: %w", err)
}

return nil
}

func resourceAwsCloudWatchEventRuleUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cloudwatcheventsconn

input, err := buildPutRuleInputStruct(d, d.Id())
_, ruleName, err := tfevents.RuleParseID(d.Id())
if err != nil {
return fmt.Errorf("Updating CloudWatch Event Rule failed: %s", err)
return err
}
input, err := buildPutRuleInputStruct(d, ruleName)
if err != nil {
return fmt.Errorf("Updating CloudWatch Events Rule (%s) failed: %w", ruleName, err)
}
log.Printf("[DEBUG] Updating CloudWatch Event Rule: %s", input)
log.Printf("[DEBUG] Updating CloudWatch Events Rule: %s", input)

// IAM Roles take some time to propagate
err = resource.Retry(30*time.Second, func() *resource.RetryError {
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 Event Rule %q", *input.Name)
log.Printf("[DEBUG] Retrying update of CloudWatch Events Rule %q", aws.StringValue(input.Name))
return resource.RetryableError(err)
}
if err != nil {
Expand All @@ -212,15 +226,15 @@ func resourceAwsCloudWatchEventRuleUpdate(d *schema.ResourceData, meta interface
}

if err != nil {
return fmt.Errorf("Updating CloudWatch Event Rule failed: %s", err)
return fmt.Errorf("Updating CloudWatch Events Rule (%s) failed: %w", ruleName, err)
}

arn := d.Get("arn").(string)
if d.HasChange("tags") {
o, n := d.GetChange("tags")

if err := keyvaluetags.CloudwatcheventsUpdateTags(conn, arn, o, n); err != nil {
return fmt.Errorf("error updating CloudwWatch Event Rule (%s) tags: %s", arn, err)
return fmt.Errorf("error updating CloudwWatch Event Rule (%s) tags: %w", arn, err)
}
}

Expand All @@ -229,12 +243,16 @@ 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())
if err != nil {
return err
}
input := &events.DeleteRuleInput{
Name: aws.String(d.Id()),
Name: aws.String(ruleName),
EventBusName: aws.String(busName),
}

err := resource.Retry(cloudWatchEventRuleDeleteRetryTimeout, func() *resource.RetryError {
err = resource.Retry(cloudWatchEventRuleDeleteRetryTimeout, func() *resource.RetryError {
_, err := conn.DeleteRule(input)

if isAWSErr(err, "ValidationException", "Rule can't be deleted since it has targets") {
Expand All @@ -253,7 +271,7 @@ func resourceAwsCloudWatchEventRuleDelete(d *schema.ResourceData, meta interface
}

if err != nil {
return fmt.Errorf("error deleting CloudWatch Event Rule (%s): %s", d.Id(), err)
return fmt.Errorf("error deleting CloudWatch Events Rule (%s): %w", d.Id(), err)
}

return nil
Expand All @@ -263,25 +281,31 @@ func buildPutRuleInputStruct(d *schema.ResourceData, name string) (*events.PutRu
input := events.PutRuleInput{
Name: aws.String(name),
}
var eventBusName string
if v, ok := d.GetOk("description"); ok {
input.Description = aws.String(v.(string))
}
if v, ok := d.GetOk("event_bus_name"); ok {
eventBusName = v.(string)
input.EventBusName = aws.String(eventBusName)
}
if v, ok := d.GetOk("event_pattern"); ok {
pattern, err := structure.NormalizeJsonString(v)
if err != nil {
return nil, fmt.Errorf("event pattern contains an invalid JSON: %s", err)
return nil, fmt.Errorf("event pattern contains an invalid JSON: %w", err)
}
input.EventPattern = aws.String(pattern)
}
if v, ok := d.GetOk("role_arn"); ok {
input.RoleArn = aws.String(v.(string))
}
if v, ok := d.GetOk("schedule_expression"); ok {
input.ScheduleExpression = aws.String(v.(string))
}

if v, ok := d.GetOk("tags"); ok {
input.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().CloudwatcheventsTags()
scheduleExpression := v.(string)
if eventBusName == tfevents.DefaultEventBusName {
input.ScheduleExpression = aws.String(scheduleExpression)
} else {
return nil, errors.New("schedule_expression can only be set on the default event bus")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Is the API error not clear in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is short-circuiting the API call. I would have set it as a ValidateFunc if they supported cross-attribute validation

}

input.State = aws.String(getStringStateFromBoolean(d.Get("is_enabled").(bool)))
Expand Down Expand Up @@ -313,7 +337,7 @@ func validateEventPatternValue() schema.SchemaValidateFunc {
return func(v interface{}, k string) (ws []string, errors []error) {
json, err := structure.NormalizeJsonString(v)
if err != nil {
errors = append(errors, fmt.Errorf("%q contains an invalid JSON: %s", k, err))
errors = append(errors, fmt.Errorf("%q contains an invalid JSON: %w", k, err))

// Invalid JSON? Return immediately,
// there is no need to collect other
Expand All @@ -323,8 +347,7 @@ func validateEventPatternValue() schema.SchemaValidateFunc {

// Check whether the normalized JSON is within the given length.
if len(json) > 2048 {
errors = append(errors, fmt.Errorf(
"%q cannot be longer than %d characters: %q", k, 2048, json))
errors = append(errors, fmt.Errorf("%q cannot be longer than %d characters: %q", k, 2048, json))
}
return
}
Expand Down
Loading