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

provider/aws: Add support for CloudWatch Events #4986

Merged
merged 7 commits into from
Feb 13, 2016
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions Godeps/Godeps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 34 additions & 29 deletions builtin/providers/aws/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/cloudtrail"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatchevents"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/codecommit"
"github.com/aws/aws-sdk-go/service/codedeploy"
Expand Down Expand Up @@ -70,35 +71,36 @@ type Config struct {
}

type AWSClient struct {
cfconn *cloudformation.CloudFormation
cloudtrailconn *cloudtrail.CloudTrail
cloudwatchconn *cloudwatch.CloudWatch
cloudwatchlogsconn *cloudwatchlogs.CloudWatchLogs
dsconn *directoryservice.DirectoryService
dynamodbconn *dynamodb.DynamoDB
ec2conn *ec2.EC2
ecrconn *ecr.ECR
ecsconn *ecs.ECS
efsconn *efs.EFS
elbconn *elb.ELB
esconn *elasticsearch.ElasticsearchService
autoscalingconn *autoscaling.AutoScaling
s3conn *s3.S3
sqsconn *sqs.SQS
snsconn *sns.SNS
redshiftconn *redshift.Redshift
r53conn *route53.Route53
region string
rdsconn *rds.RDS
iamconn *iam.IAM
kinesisconn *kinesis.Kinesis
firehoseconn *firehose.Firehose
elasticacheconn *elasticache.ElastiCache
lambdaconn *lambda.Lambda
opsworksconn *opsworks.OpsWorks
glacierconn *glacier.Glacier
codedeployconn *codedeploy.CodeDeploy
codecommitconn *codecommit.CodeCommit
cfconn *cloudformation.CloudFormation
cloudtrailconn *cloudtrail.CloudTrail
cloudwatchconn *cloudwatch.CloudWatch
cloudwatchlogsconn *cloudwatchlogs.CloudWatchLogs
cloudwatcheventsconn *cloudwatchevents.CloudWatchEvents
dsconn *directoryservice.DirectoryService
dynamodbconn *dynamodb.DynamoDB
ec2conn *ec2.EC2
ecrconn *ecr.ECR
ecsconn *ecs.ECS
efsconn *efs.EFS
elbconn *elb.ELB
esconn *elasticsearch.ElasticsearchService
autoscalingconn *autoscaling.AutoScaling
s3conn *s3.S3
sqsconn *sqs.SQS
snsconn *sns.SNS
redshiftconn *redshift.Redshift
r53conn *route53.Route53
region string
rdsconn *rds.RDS
iamconn *iam.IAM
kinesisconn *kinesis.Kinesis
firehoseconn *firehose.Firehose
elasticacheconn *elasticache.ElastiCache
lambdaconn *lambda.Lambda
opsworksconn *opsworks.OpsWorks
glacierconn *glacier.Glacier
codedeployconn *codedeploy.CodeDeploy
codecommitconn *codecommit.CodeCommit
}

// Client configures and returns a fully initialized AWSClient
Expand Down Expand Up @@ -256,6 +258,9 @@ func (c *Config) Client() (interface{}, error) {
log.Println("[INFO] Initializing CloudWatch SDK connection")
client.cloudwatchconn = cloudwatch.New(sess)

log.Println("[INFO] Initializing CloudWatch Events connection")
client.cloudwatcheventsconn = cloudwatchevents.New(sess)

log.Println("[INFO] Initializing CloudTrail connection")
client.cloudtrailconn = cloudtrail.New(sess)

Expand Down
2 changes: 2 additions & 0 deletions builtin/providers/aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ func Provider() terraform.ResourceProvider {
"aws_autoscaling_schedule": resourceAwsAutoscalingSchedule(),
"aws_cloudformation_stack": resourceAwsCloudFormationStack(),
"aws_cloudtrail": resourceAwsCloudTrail(),
"aws_cloudwatch_event_rule": resourceAwsCloudWatchEventRule(),
"aws_cloudwatch_event_target": resourceAwsCloudWatchEventTarget(),
"aws_cloudwatch_log_group": resourceAwsCloudWatchLogGroup(),
"aws_autoscaling_lifecycle_hook": resourceAwsAutoscalingLifecycleHook(),
"aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(),
Expand Down
252 changes: 252 additions & 0 deletions builtin/providers/aws/resource_aws_cloudwatch_event_rule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package aws

import (
"fmt"
"log"
"regexp"
"time"

"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"

"github.com/aws/aws-sdk-go/aws"
events "github.com/aws/aws-sdk-go/service/cloudwatchevents"
)

func resourceAwsCloudWatchEventRule() *schema.Resource {
return &schema.Resource{
Create: resourceAwsCloudWatchEventRuleCreate,
Read: resourceAwsCloudWatchEventRuleRead,
Update: resourceAwsCloudWatchEventRuleUpdate,
Delete: resourceAwsCloudWatchEventRuleDelete,

Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateCloudWatchEventRuleName,
},
"schedule_expression": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateMaxLength(256),
Copy link
Contributor

Choose a reason for hiding this comment

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

Love this little helper! This will be able to be used elsewhere :)

},
"event_pattern": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateMaxLength(2048),
StateFunc: normalizeJson,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateMaxLength(512),
},
"role_arn": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateMaxLength(1600),
},
"is_enabled": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"arn": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}

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

input := buildPutRuleInputStruct(d)
log.Printf("[DEBUG] Creating CloudWatch Event Rule: %s", input)

// IAM Roles take some time to propagate
var out *events.PutRuleOutput
err := resource.Retry(30*time.Second, func() error {
var err error
out, err = conn.PutRule(input)
pattern := regexp.MustCompile("cannot be assumed by principal '[a-z]+\\.amazonaws\\.com'\\.$")
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "ValidationException" && pattern.MatchString(awsErr.Message()) {
log.Printf("[DEBUG] Retrying creation of CloudWatch Event Rule %q", *input.Name)
return err
}
}
return &resource.RetryError{
Err: err,
}
}
return nil
})
if err != nil {
return fmt.Errorf("Creating CloudWatch Event Rule failed: %s", err)
}

d.Set("arn", out.RuleArn)
d.SetId(d.Get("name").(string))

log.Printf("[INFO] CloudWatch Event Rule %q created", *out.RuleArn)

return resourceAwsCloudWatchEventRuleUpdate(d, meta)
}

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

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() == "ResourceNotFoundException" {
log.Printf("[WARN] Removing CloudWatch Event Rule %q because it's gone.", d.Id())
d.SetId("")
return nil
}
}
if err != nil {
return err
}
log.Printf("[DEBUG] Found Event Rule: %s", out)

d.Set("arn", out.Arn)
d.Set("description", out.Description)
if out.EventPattern != nil {
d.Set("event_pattern", normalizeJson(*out.EventPattern))
}
d.Set("name", out.Name)
d.Set("role_arn", out.RoleArn)
d.Set("schedule_expression", out.ScheduleExpression)

boolState, err := getBooleanStateFromString(*out.State)
if err != nil {
return err
}
log.Printf("[DEBUG] Setting boolean state: %t", boolState)
d.Set("is_enabled", boolState)

return nil
}

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

if d.HasChange("is_enabled") && d.Get("is_enabled").(bool) {
log.Printf("[DEBUG] Enabling CloudWatch Event Rule %q", d.Id())
_, err := conn.EnableRule(&events.EnableRuleInput{
Name: aws.String(d.Id()),
})
if err != nil {
return err
}
log.Printf("[DEBUG] CloudWatch Event Rule (%q) enabled", d.Id())
}

input := buildPutRuleInputStruct(d)
log.Printf("[DEBUG] Updating CloudWatch Event Rule: %s", input)

// IAM Roles take some time to propagate
var out *events.PutRuleOutput
err := resource.Retry(30*time.Second, func() error {
var err error
out, err = conn.PutRule(input)
pattern := regexp.MustCompile("cannot be assumed by principal '[a-z]+\\.amazonaws\\.com'\\.$")
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "ValidationException" && pattern.MatchString(awsErr.Message()) {
log.Printf("[DEBUG] Retrying update of CloudWatch Event Rule %q", *input.Name)
return err
}
}
return &resource.RetryError{
Err: err,
}
}
return nil
})
if err != nil {
return fmt.Errorf("Updating CloudWatch Event Rule failed: %s", err)
}

if d.HasChange("is_enabled") && !d.Get("is_enabled").(bool) {
log.Printf("[DEBUG] Disabling CloudWatch Event Rule %q", d.Id())
_, err := conn.DisableRule(&events.DisableRuleInput{
Name: aws.String(d.Id()),
})
if err != nil {
return err
}
log.Printf("[DEBUG] CloudWatch Event Rule (%q) disabled", d.Id())
}

return resourceAwsCloudWatchEventRuleRead(d, meta)
}

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

log.Printf("[INFO] Deleting CloudWatch Event Rule: %s", d.Id())
_, err := conn.DeleteRule(&events.DeleteRuleInput{
Name: aws.String(d.Id()),
})
if err != nil {
return fmt.Errorf("Error deleting CloudWatch Event Rule: %s", err)
}
log.Println("[INFO] CloudWatch Event Rule deleted")

d.SetId("")

return nil
}

func buildPutRuleInputStruct(d *schema.ResourceData) *events.PutRuleInput {
input := events.PutRuleInput{
Name: aws.String(d.Get("name").(string)),
}
if v, ok := d.GetOk("description"); ok {
input.Description = aws.String(v.(string))
}
if v, ok := d.GetOk("event_pattern"); ok {
input.EventPattern = aws.String(v.(string))
}
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))
}

input.State = aws.String(getStringStateFromBoolean(d.Get("is_enabled").(bool)))

return &input
}

// State is represented as (ENABLED|DISABLED) in the API
func getBooleanStateFromString(state string) (bool, error) {
if state == "ENABLED" {
Copy link
Contributor

Choose a reason for hiding this comment

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

are we positive we will always get back uppercase?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, it is documented like that in AWS docs: http://docs.aws.amazon.com/AmazonCloudWatchEvents/latest/APIReference/API_PutRule.html#API_PutRule_RequestSyntax but I agree that trusting the docs may not always be good idea and tracking down such bug here might not be easy with this if block.

I will add error reporting for unexpected values.

return true, nil
} else if state == "DISABLED" {
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 "ENABLED"
}
return "DISABLED"
}
Loading