diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 4b0ed43883..b627c36f39 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -262,6 +262,11 @@ "Comment": "v1.1.0", "Rev": "be2ec39e520e3c4088c0c6288055bdc8184a89ee" }, + { + "ImportPath": "github.com/aws/aws-sdk-go/service/cloudwatchevents", + "Comment": "v1.1.0", + "Rev": "be2ec39e520e3c4088c0c6288055bdc8184a89ee" + }, { "ImportPath": "github.com/aws/aws-sdk-go/service/cloudwatchlogs", "Comment": "v1.1.0", diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 5178b41191..33645b6a1d 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -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" @@ -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 @@ -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) diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index ffa5a4d7c9..3b7bb79f6e 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -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(), diff --git a/builtin/providers/aws/resource_aws_cloudwatch_event_rule.go b/builtin/providers/aws/resource_aws_cloudwatch_event_rule.go new file mode 100644 index 0000000000..652636b683 --- /dev/null +++ b/builtin/providers/aws/resource_aws_cloudwatch_event_rule.go @@ -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), + }, + "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" { + 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" +} diff --git a/builtin/providers/aws/resource_aws_cloudwatch_event_rule_test.go b/builtin/providers/aws/resource_aws_cloudwatch_event_rule_test.go new file mode 100644 index 0000000000..5bedff782b --- /dev/null +++ b/builtin/providers/aws/resource_aws_cloudwatch_event_rule_test.go @@ -0,0 +1,208 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + events "github.com/aws/aws-sdk-go/service/cloudwatchevents" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSCloudWatchEventRule_basic(t *testing.T) { + var rule events.DescribeRuleOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSCloudWatchEventRuleConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventRuleExists("aws_cloudwatch_event_rule.foo", &rule), + resource.TestCheckResourceAttr("aws_cloudwatch_event_rule.foo", "name", "tf-acc-cw-event-rule"), + ), + }, + resource.TestStep{ + Config: testAccAWSCloudWatchEventRuleConfigModified, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventRuleExists("aws_cloudwatch_event_rule.foo", &rule), + resource.TestCheckResourceAttr("aws_cloudwatch_event_rule.foo", "name", "tf-acc-cw-event-rule-mod"), + ), + }, + }, + }) +} + +func TestAccAWSCloudWatchEventRule_full(t *testing.T) { + var rule events.DescribeRuleOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSCloudWatchEventRuleConfig_full, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventRuleExists("aws_cloudwatch_event_rule.moobar", &rule), + resource.TestCheckResourceAttr("aws_cloudwatch_event_rule.moobar", "name", "tf-acc-cw-event-rule-full"), + resource.TestCheckResourceAttr("aws_cloudwatch_event_rule.moobar", "schedule_expression", "rate(5 minutes)"), + resource.TestCheckResourceAttr("aws_cloudwatch_event_rule.moobar", "event_pattern", "{\"source\":[\"aws.ec2\"]}"), + resource.TestCheckResourceAttr("aws_cloudwatch_event_rule.moobar", "description", "He's not dead, he's just resting!"), + resource.TestCheckResourceAttr("aws_cloudwatch_event_rule.moobar", "role_arn", ""), + testAccCheckCloudWatchEventRuleEnabled("aws_cloudwatch_event_rule.moobar", "DISABLED", &rule), + ), + }, + }, + }) +} + +func TestAccAWSCloudWatchEventRule_enable(t *testing.T) { + var rule events.DescribeRuleOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudWatchEventRuleDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSCloudWatchEventRuleConfigEnabled, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventRuleExists("aws_cloudwatch_event_rule.moo", &rule), + testAccCheckCloudWatchEventRuleEnabled("aws_cloudwatch_event_rule.moo", "ENABLED", &rule), + ), + }, + resource.TestStep{ + Config: testAccAWSCloudWatchEventRuleConfigDisabled, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventRuleExists("aws_cloudwatch_event_rule.moo", &rule), + testAccCheckCloudWatchEventRuleEnabled("aws_cloudwatch_event_rule.moo", "DISABLED", &rule), + ), + }, + resource.TestStep{ + Config: testAccAWSCloudWatchEventRuleConfigEnabled, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventRuleExists("aws_cloudwatch_event_rule.moo", &rule), + testAccCheckCloudWatchEventRuleEnabled("aws_cloudwatch_event_rule.moo", "ENABLED", &rule), + ), + }, + }, + }) +} + +func testAccCheckCloudWatchEventRuleExists(n string, rule *events.DescribeRuleOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).cloudwatcheventsconn + params := events.DescribeRuleInput{ + Name: aws.String(rs.Primary.ID), + } + resp, err := conn.DescribeRule(¶ms) + if err != nil { + return err + } + if resp == nil { + return fmt.Errorf("Rule not found") + } + + *rule = *resp + + return nil + } +} + +func testAccCheckCloudWatchEventRuleEnabled(n string, desired string, rule *events.DescribeRuleOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).cloudwatcheventsconn + params := events.DescribeRuleInput{ + Name: aws.String(rs.Primary.ID), + } + resp, err := conn.DescribeRule(¶ms) + + if err != nil { + return err + } + if *resp.State != desired { + return fmt.Errorf("Expected state %q, given %q", desired, *resp.State) + } + + return nil + } +} + +func testAccCheckAWSCloudWatchEventRuleDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudwatcheventsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudwatch_event_rule" { + continue + } + + params := events.DescribeRuleInput{ + Name: aws.String(rs.Primary.ID), + } + + resp, err := conn.DescribeRule(¶ms) + + if err == nil { + return fmt.Errorf("CloudWatch Event Rule %q still exists: %s", + rs.Primary.ID, resp) + } + } + + return nil +} + +var testAccAWSCloudWatchEventRuleConfig = ` +resource "aws_cloudwatch_event_rule" "foo" { + name = "tf-acc-cw-event-rule" + schedule_expression = "rate(1 hour)" +} +` + +var testAccAWSCloudWatchEventRuleConfigEnabled = ` +resource "aws_cloudwatch_event_rule" "moo" { + name = "tf-acc-cw-event-rule-state" + schedule_expression = "rate(1 hour)" +} +` +var testAccAWSCloudWatchEventRuleConfigDisabled = ` +resource "aws_cloudwatch_event_rule" "moo" { + name = "tf-acc-cw-event-rule-state" + schedule_expression = "rate(1 hour)" + is_enabled = false +} +` + +var testAccAWSCloudWatchEventRuleConfigModified = ` +resource "aws_cloudwatch_event_rule" "foo" { + name = "tf-acc-cw-event-rule-mod" + schedule_expression = "rate(1 hour)" +} +` + +var testAccAWSCloudWatchEventRuleConfig_full = ` +resource "aws_cloudwatch_event_rule" "moobar" { + name = "tf-acc-cw-event-rule-full" + schedule_expression = "rate(5 minutes)" + event_pattern = < 0 { + return fmt.Errorf("Creating CloudWatch Event Target failed: %s", + out.FailedEntries) + } + + log.Printf("[INFO] CloudWatch Event Target %q created", d.Id()) + + return resourceAwsCloudWatchEventTargetRead(d, meta) +} + +func resourceAwsCloudWatchEventTargetRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatcheventsconn + + t, err := findEventTargetById( + d.Get("target_id").(string), + d.Get("rule").(string), + nil, conn) + 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 + } + return err + } + log.Printf("[DEBUG] Found Event Target: %s", t) + + d.Set("arn", t.Arn) + d.Set("target_id", t.Id) + d.Set("input", t.Input) + d.Set("input_path", t.InputPath) + + return nil +} + +func findEventTargetById(id, rule string, nextToken *string, conn *events.CloudWatchEvents) ( + *events.Target, error) { + input := events.ListTargetsByRuleInput{ + Rule: aws.String(rule), + NextToken: nextToken, + Limit: aws.Int64(100), // Set limit to allowed maximum to prevent API throttling + } + log.Printf("[DEBUG] Reading CloudWatch Event Target: %s", input) + out, err := conn.ListTargetsByRule(&input) + if err != nil { + return nil, err + } + + for _, t := range out.Targets { + if *t.Id == id { + return t, nil + } + } + + if out.NextToken != nil { + return findEventTargetById(id, rule, nextToken, conn) + } + + return nil, fmt.Errorf("CloudWatch Event Target %q (%q) not found", id, rule) +} + +func resourceAwsCloudWatchEventTargetUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatcheventsconn + + input := buildPutTargetInputStruct(d) + log.Printf("[DEBUG] Updating CloudWatch Event Target: %s", input) + _, err := conn.PutTargets(input) + if err != nil { + return fmt.Errorf("Updating CloudWatch Event Target failed: %s", err) + } + + return resourceAwsCloudWatchEventTargetRead(d, meta) +} + +func resourceAwsCloudWatchEventTargetDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatcheventsconn + + input := events.RemoveTargetsInput{ + Ids: []*string{aws.String(d.Get("target_id").(string))}, + Rule: aws.String(d.Get("rule").(string)), + } + log.Printf("[INFO] Deleting CloudWatch Event Target: %s", input) + _, err := conn.RemoveTargets(&input) + if err != nil { + return fmt.Errorf("Error deleting CloudWatch Event Target: %s", err) + } + log.Println("[INFO] CloudWatch Event Target deleted") + + d.SetId("") + + return nil +} + +func buildPutTargetInputStruct(d *schema.ResourceData) *events.PutTargetsInput { + e := &events.Target{ + Arn: aws.String(d.Get("arn").(string)), + Id: aws.String(d.Get("target_id").(string)), + } + + if v, ok := d.GetOk("input"); ok { + e.Input = aws.String(v.(string)) + } + if v, ok := d.GetOk("input_path"); ok { + e.InputPath = aws.String(v.(string)) + } + + input := events.PutTargetsInput{ + Rule: aws.String(d.Get("rule").(string)), + Targets: []*events.Target{e}, + } + + return &input +} diff --git a/builtin/providers/aws/resource_aws_cloudwatch_event_target_test.go b/builtin/providers/aws/resource_aws_cloudwatch_event_target_test.go new file mode 100644 index 0000000000..56fffdeab1 --- /dev/null +++ b/builtin/providers/aws/resource_aws_cloudwatch_event_target_test.go @@ -0,0 +1,203 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + events "github.com/aws/aws-sdk-go/service/cloudwatchevents" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSCloudWatchEventTarget_basic(t *testing.T) { + var target events.Target + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudWatchEventTargetDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSCloudWatchEventTargetConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventTargetExists("aws_cloudwatch_event_target.moobar", &target), + resource.TestCheckResourceAttr("aws_cloudwatch_event_target.moobar", "rule", "tf-acc-cw-event-rule-basic"), + resource.TestCheckResourceAttr("aws_cloudwatch_event_target.moobar", "target_id", "tf-acc-cw-target-basic"), + resource.TestMatchResourceAttr("aws_cloudwatch_event_target.moobar", "arn", + regexp.MustCompile(":tf-acc-moon$")), + ), + }, + resource.TestStep{ + Config: testAccAWSCloudWatchEventTargetConfigModified, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventTargetExists("aws_cloudwatch_event_target.moobar", &target), + resource.TestCheckResourceAttr("aws_cloudwatch_event_target.moobar", "rule", "tf-acc-cw-event-rule-basic"), + resource.TestCheckResourceAttr("aws_cloudwatch_event_target.moobar", "target_id", "tf-acc-cw-target-modified"), + resource.TestMatchResourceAttr("aws_cloudwatch_event_target.moobar", "arn", + regexp.MustCompile(":tf-acc-sun$")), + ), + }, + }, + }) +} + +func TestAccAWSCloudWatchEventTarget_full(t *testing.T) { + var target events.Target + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudWatchEventTargetDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSCloudWatchEventTargetConfig_full, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchEventTargetExists("aws_cloudwatch_event_target.foobar", &target), + resource.TestCheckResourceAttr("aws_cloudwatch_event_target.foobar", "rule", "tf-acc-cw-event-rule-full"), + resource.TestCheckResourceAttr("aws_cloudwatch_event_target.foobar", "target_id", "tf-acc-cw-target-full"), + resource.TestMatchResourceAttr("aws_cloudwatch_event_target.foobar", "arn", + regexp.MustCompile("^arn:aws:kinesis:.*:stream/terraform-kinesis-test$")), + resource.TestCheckResourceAttr("aws_cloudwatch_event_target.foobar", "input", "{ \"source\": [\"aws.cloudtrail\"] }\n"), + resource.TestCheckResourceAttr("aws_cloudwatch_event_target.foobar", "input_path", ""), + ), + }, + }, + }) +} + +func testAccCheckCloudWatchEventTargetExists(n string, rule *events.Target) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).cloudwatcheventsconn + t, err := findEventTargetById(rs.Primary.Attributes["target_id"], + rs.Primary.Attributes["rule"], nil, conn) + if err != nil { + return fmt.Errorf("Event Target not found: %s", err) + } + + *rule = *t + + return nil + } +} + +func testAccCheckAWSCloudWatchEventTargetDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudwatcheventsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudwatch_event_target" { + continue + } + + t, err := findEventTargetById(rs.Primary.Attributes["target_id"], + rs.Primary.Attributes["rule"], nil, conn) + if err == nil { + return fmt.Errorf("CloudWatch Event Target %q still exists: %s", + rs.Primary.ID, t) + } + } + + return nil +} + +var testAccAWSCloudWatchEventTargetConfig = ` +resource "aws_cloudwatch_event_rule" "foo" { + name = "tf-acc-cw-event-rule-basic" + schedule_expression = "rate(1 hour)" +} + +resource "aws_cloudwatch_event_target" "moobar" { + rule = "${aws_cloudwatch_event_rule.foo.name}" + target_id = "tf-acc-cw-target-basic" + arn = "${aws_sns_topic.moon.arn}" +} + +resource "aws_sns_topic" "moon" { + name = "tf-acc-moon" +} +` + +var testAccAWSCloudWatchEventTargetConfigModified = ` +resource "aws_cloudwatch_event_rule" "foo" { + name = "tf-acc-cw-event-rule-basic" + schedule_expression = "rate(1 hour)" +} + +resource "aws_cloudwatch_event_target" "moobar" { + rule = "${aws_cloudwatch_event_rule.foo.name}" + target_id = "tf-acc-cw-target-modified" + arn = "${aws_sns_topic.sun.arn}" +} + +resource "aws_sns_topic" "sun" { + name = "tf-acc-sun" +} +` + +var testAccAWSCloudWatchEventTargetConfig_full = ` +resource "aws_cloudwatch_event_rule" "foo" { + name = "tf-acc-cw-event-rule-full" + schedule_expression = "rate(1 hour)" + role_arn = "${aws_iam_role.role.arn}" +} + +resource "aws_iam_role" "role" { + name = "test_role" + assume_role_policy = < 64 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 64 characters: %q", k, value)) + } + + // http://docs.aws.amazon.com/AmazonCloudWatchEvents/latest/APIReference/API_PutRule.html + pattern := `^[\.\-_A-Za-z0-9]+$` + if !regexp.MustCompile(pattern).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q doesn't comply with restrictions (%q): %q", + k, pattern, value)) + } + + return +} + +func validateMaxLength(length int) schema.SchemaValidateFunc { + return func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if len(value) > length { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than %d characters: %q", k, length, value)) + } + return + } +} + +func validateCloudWatchEventTargetId(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if len(value) > 64 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 64 characters: %q", k, value)) + } + + // http://docs.aws.amazon.com/AmazonCloudWatchEvents/latest/APIReference/API_Target.html + pattern := `^[\.\-_A-Za-z0-9]+$` + if !regexp.MustCompile(pattern).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q doesn't comply with restrictions (%q): %q", + k, pattern, value)) + } + + return +} diff --git a/builtin/providers/aws/validators_test.go b/builtin/providers/aws/validators_test.go index 0b2ee011ea..f801bfcb20 100644 --- a/builtin/providers/aws/validators_test.go +++ b/builtin/providers/aws/validators_test.go @@ -43,3 +43,30 @@ func TestValidateEcrRepositoryName(t *testing.T) { } } } + +func TestValidateCloudWatchEventRuleName(t *testing.T) { + validNames := []string{ + "HelloWorl_d", + "hello-world", + "hello.World0125", + } + for _, v := range validNames { + _, errors := validateCloudWatchEventRuleName(v, "name") + if len(errors) != 0 { + t.Fatalf("%q should be a valid CW event rule name: %q", v, errors) + } + } + + invalidNames := []string{ + "special@character", + "slash/in-the-middle", + // Length > 64 + "TooLooooooooooooooooooooooooooooooooooooooooooooooooooooooongName", + } + for _, v := range invalidNames { + _, errors := validateCloudWatchEventRuleName(v, "name") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid CW event rule name", v) + } + } +} diff --git a/examples/aws-cloudwatch-events/kinesis/README.md b/examples/aws-cloudwatch-events/kinesis/README.md new file mode 100644 index 0000000000..2dcb829cc0 --- /dev/null +++ b/examples/aws-cloudwatch-events/kinesis/README.md @@ -0,0 +1,14 @@ +# CloudWatch Event sent to Kinesis Stream + +This example sets up a CloudWatch Event Rule with a Target and IAM Role & Policy +to send all autoscaling events into Kinesis stream for further examination. + +See more details about [CloudWatch Events](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/WhatIsCloudWatchEvents.html) +in the official AWS docs. + +## How to run the example + +``` +terraform apply \ + -var=aws_region=us-west-2 +``` diff --git a/examples/aws-cloudwatch-events/kinesis/main.tf b/examples/aws-cloudwatch-events/kinesis/main.tf new file mode 100644 index 0000000000..0e7a220528 --- /dev/null +++ b/examples/aws-cloudwatch-events/kinesis/main.tf @@ -0,0 +1,72 @@ +provider "aws" { + region = "${var.aws_region}" +} + +resource "aws_cloudwatch_event_rule" "foo" { + name = "${var.rule_name}" + event_pattern = < **Note:** `input` and `input_path` are mutually exclusive options. + +The following arguments are supported: + +* `rule` - (Required) The name of the rule you want to add targets to. +* `target_id` - (Required) The unique target assignment ID. +* `arn` - (Required) The Amazon Resource Name (ARN) associated of the target. +* `input` - (Optional) Valid JSON text passed to the target. +* `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. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 4f1319ddab..c9298f34a4 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -31,6 +31,13 @@ > CloudWatch Resources