From 1f80ec48d0b81b840dc82cc28b969865eaa8bde9 Mon Sep 17 00:00:00 2001 From: Kraig Amador Date: Thu, 5 May 2016 03:14:25 -0700 Subject: [PATCH] Added RDS event subscriptions (#6367) --- builtin/providers/aws/provider.go | 1 + .../aws/resource_aws_db_event_subscription.go | 333 ++++++++++++++++++ ...resource_aws_db_event_subscription_test.go | 162 +++++++++ builtin/providers/aws/validators.go | 13 + builtin/providers/aws/validators_test.go | 33 ++ .../aws/r/db_event_subscription.html.markdown | 34 ++ website/source/layouts/aws.erb | 4 + 7 files changed, 580 insertions(+) create mode 100644 builtin/providers/aws/resource_aws_db_event_subscription.go create mode 100644 builtin/providers/aws/resource_aws_db_event_subscription_test.go create mode 100644 website/source/docs/providers/aws/r/db_event_subscription.html.markdown diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 343c5015a..6e9505c2e 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -145,6 +145,7 @@ func Provider() terraform.ResourceProvider { "aws_codedeploy_deployment_group": resourceAwsCodeDeployDeploymentGroup(), "aws_codecommit_repository": resourceAwsCodeCommitRepository(), "aws_customer_gateway": resourceAwsCustomerGateway(), + "aws_db_event_subscription": resourceAwsDbEventSubscription(), "aws_db_instance": resourceAwsDbInstance(), "aws_db_parameter_group": resourceAwsDbParameterGroup(), "aws_db_security_group": resourceAwsDbSecurityGroup(), diff --git a/builtin/providers/aws/resource_aws_db_event_subscription.go b/builtin/providers/aws/resource_aws_db_event_subscription.go new file mode 100644 index 000000000..3f86aad90 --- /dev/null +++ b/builtin/providers/aws/resource_aws_db_event_subscription.go @@ -0,0 +1,333 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsDbEventSubscription() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsDbEventSubscriptionCreate, + Read: resourceAwsDbEventSubscriptionRead, + Update: resourceAwsDbEventSubscriptionUpdate, + Delete: resourceAwsDbEventSubscriptionDelete, + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateDbEventSubscriptionName, + }, + "sns_topic": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "event_categories": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "source_ids": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + // ValidateFunc: validateDbEventSubscriptionSourceIds, + // requires source_type to be set, does not seem to be a way to validate this + }, + "source_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "customer_aws_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchema(), + }, + } +} + +func resourceAwsDbEventSubscriptionCreate(d *schema.ResourceData, meta interface{}) error { + rdsconn := meta.(*AWSClient).rdsconn + name := d.Get("name").(string) + tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{})) + + sourceIdsSet := d.Get("source_ids").(*schema.Set) + sourceIds := make([]*string, sourceIdsSet.Len()) + for i, sourceId := range sourceIdsSet.List() { + sourceIds[i] = aws.String(sourceId.(string)) + } + + eventCategoriesSet := d.Get("event_categories").(*schema.Set) + eventCategories := make([]*string, eventCategoriesSet.Len()) + for i, eventCategory := range eventCategoriesSet.List() { + eventCategories[i] = aws.String(eventCategory.(string)) + } + + request := &rds.CreateEventSubscriptionInput{ + SubscriptionName: aws.String(name), + SnsTopicArn: aws.String(d.Get("sns_topic").(string)), + Enabled: aws.Bool(d.Get("enabled").(bool)), + SourceIds: sourceIds, + SourceType: aws.String(d.Get("source_type").(string)), + EventCategories: eventCategories, + Tags: tags, + } + + log.Println("[DEBUG] Create RDS Event Subscription:", request) + + _, err := rdsconn.CreateEventSubscription(request) + if err != nil { + return fmt.Errorf("Error creating RDS Event Subscription %s: %s", name, err) + } + + log.Println( + "[INFO] Waiting for RDS Event Subscription to be ready") + + stateConf := &resource.StateChangeConf{ + Pending: []string{"creating"}, + Target: []string{"active"}, + Refresh: resourceAwsDbEventSubscriptionRefreshFunc(d, meta.(*AWSClient).rdsconn), + Timeout: 40 * time.Minute, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, // Wait 30 secs before starting + } + + // Wait, catching any errors + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Creating RDS Event Subscription %s failed: %s", d.Id(), err) + } + + return resourceAwsDbEventSubscriptionRead(d, meta) +} + +func resourceAwsDbEventSubscriptionRead(d *schema.ResourceData, meta interface{}) error { + sub, err := resourceAwsDbEventSubscriptionRetrieve(d.Get("name").(string), meta.(*AWSClient).rdsconn) + if err != nil { + return fmt.Errorf("Error retrieving RDS Event Subscription %s: %s", d.Id(), err) + } + if sub == nil { + d.SetId("") + return nil + } + + d.SetId(*sub.CustSubscriptionId) + if err := d.Set("name", sub.CustSubscriptionId); err != nil { + return err + } + if err := d.Set("sns_topic", sub.SnsTopicArn); err != nil { + return err + } + if err := d.Set("source_type", sub.SourceType); err != nil { + return err + } + if err := d.Set("enabled", sub.Enabled); err != nil { + return err + } + if err := d.Set("source_ids", flattenStringList(sub.SourceIdsList)); err != nil { + return err + } + if err := d.Set("event_categories", flattenStringList(sub.EventCategoriesList)); err != nil { + return err + } + if err := d.Set("customer_aws_id", sub.CustomerAwsId); err != nil { + return err + } + + // list tags for resource + // set tags + conn := meta.(*AWSClient).rdsconn + arn := buildRDSEventSubscriptionARN(d.Get("customer_aws_id").(string), d.Id(), meta.(*AWSClient).region) + resp, err := conn.ListTagsForResource(&rds.ListTagsForResourceInput{ + ResourceName: aws.String(arn), + }) + + if err != nil { + log.Printf("[DEBUG] Error retrieving tags for ARN: %s", arn) + } + + var dt []*rds.Tag + if len(resp.TagList) > 0 { + dt = resp.TagList + } + d.Set("tags", tagsToMapRDS(dt)) + + return nil +} + +func resourceAwsDbEventSubscriptionRetrieve( + name string, rdsconn *rds.RDS) (*rds.EventSubscription, error) { + + request := &rds.DescribeEventSubscriptionsInput{ + SubscriptionName: aws.String(name), + } + + describeResp, err := rdsconn.DescribeEventSubscriptions(request) + if err != nil { + if rdserr, ok := err.(awserr.Error); ok && rdserr.Code() == "SubscriptionNotFound" { + log.Printf("[WARN] No RDS Event Subscription by name (%s) found", name) + return nil, nil + } + return nil, fmt.Errorf("Error reading RDS Event Subscription %s: %s", name, err) + } + + if len(describeResp.EventSubscriptionsList) != 1 { + return nil, fmt.Errorf("Unable to find RDS Event Subscription: %#v", describeResp.EventSubscriptionsList) + } + + return describeResp.EventSubscriptionsList[0], nil +} + +func resourceAwsDbEventSubscriptionUpdate(d *schema.ResourceData, meta interface{}) error { + rdsconn := meta.(*AWSClient).rdsconn + + d.Partial(true) + requestUpdate := false + + req := &rds.ModifyEventSubscriptionInput{ + SubscriptionName: aws.String(d.Id()), + } + + if d.HasChange("event_categories") { + eventCategoriesSet := d.Get("event_categories").(*schema.Set) + req.EventCategories = make([]*string, eventCategoriesSet.Len()) + for i, eventCategory := range eventCategoriesSet.List() { + req.EventCategories[i] = aws.String(eventCategory.(string)) + } + requestUpdate = true + } + + if d.HasChange("enabled") { + req.Enabled = aws.Bool(d.Get("enabled").(bool)) + requestUpdate = true + } + + if d.HasChange("sns_topic") { + req.SnsTopicArn = aws.String(d.Get("sns_topic").(string)) + requestUpdate = true + } + + if d.HasChange("source_type") { + req.SourceType = aws.String(d.Get("source_type").(string)) + requestUpdate = true + } + + log.Printf("[DEBUG] Send RDS Event Subscription modification request: %#v", requestUpdate) + if requestUpdate { + log.Printf("[DEBUG] RDS Event Subscription modification request: %#v", req) + _, err := rdsconn.ModifyEventSubscription(req) + if err != nil { + return fmt.Errorf("Modifying RDS Event Subscription %s failed: %s", d.Id(), err) + } + + log.Println( + "[INFO] Waiting for RDS Event Subscription modification to finish") + + stateConf := &resource.StateChangeConf{ + Pending: []string{"modifying"}, + Target: []string{"active"}, + Refresh: resourceAwsDbEventSubscriptionRefreshFunc(d, meta.(*AWSClient).rdsconn), + Timeout: 40 * time.Minute, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, // Wait 30 secs before starting + } + + // Wait, catching any errors + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Modifying RDS Event Subscription %s failed: %s", d.Id(), err) + } + d.SetPartial("event_categories") + d.SetPartial("enabled") + d.SetPartial("sns_topic") + d.SetPartial("source_type") + } + + arn := buildRDSEventSubscriptionARN(d.Get("customer_aws_id").(string), d.Id(), meta.(*AWSClient).region) + if err := setTagsRDS(rdsconn, d, arn); err != nil { + return err + } else { + d.SetPartial("tags") + } + d.Partial(false) + + return nil +} + +func resourceAwsDbEventSubscriptionDelete(d *schema.ResourceData, meta interface{}) error { + rdsconn := meta.(*AWSClient).rdsconn + deleteOpts := rds.DeleteEventSubscriptionInput{ + SubscriptionName: aws.String(d.Id()), + } + + if _, err := rdsconn.DeleteEventSubscription(&deleteOpts); err != nil { + rdserr, ok := err.(awserr.Error) + if !ok { + return fmt.Errorf("Error deleting RDS Event Subscription %s: %s", d.Id(), err) + } + + if rdserr.Code() != "DBEventSubscriptionNotFoundFault" { + log.Printf("[WARN] RDS Event Subscription %s missing during delete", d.Id()) + return fmt.Errorf("Error deleting RDS Event Subscription %s: %s", d.Id(), err) + } + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"deleting"}, + Target: []string{}, + Refresh: resourceAwsDbEventSubscriptionRefreshFunc(d, meta.(*AWSClient).rdsconn), + Timeout: 40 * time.Minute, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, // Wait 30 secs before starting + } + _, err := stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error deleting RDS Event Subscription %s: %s", d.Id(), err) + } + return err +} + +func resourceAwsDbEventSubscriptionRefreshFunc( + d *schema.ResourceData, + rdsconn *rds.RDS) resource.StateRefreshFunc { + + return func() (interface{}, string, error) { + sub, err := resourceAwsDbEventSubscriptionRetrieve(d.Get("name").(string), rdsconn) + + if err != nil { + log.Printf("Error on retrieving DB Event Subscription when waiting: %s", err) + return nil, "", err + } + + if sub == nil { + return nil, "", nil + } + + if sub.Status != nil { + log.Printf("[DEBUG] DB Event Subscription status for %s: %s", d.Id(), *sub.Status) + } + + return sub, *sub.Status, nil + } +} + +func buildRDSEventSubscriptionARN(customerAwsId, subscriptionId, region string) string { + arn := fmt.Sprintf("arn:aws:rds:%s:%s:es:%s", region, customerAwsId, subscriptionId) + return arn +} diff --git a/builtin/providers/aws/resource_aws_db_event_subscription_test.go b/builtin/providers/aws/resource_aws_db_event_subscription_test.go new file mode 100644 index 000000000..26de043c9 --- /dev/null +++ b/builtin/providers/aws/resource_aws_db_event_subscription_test.go @@ -0,0 +1,162 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSDBEventSubscription_basicUpdate(t *testing.T) { + var v rds.EventSubscription + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSDBEventSubscriptionDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSDBEventSubscriptionConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSDBEventSubscriptionExists("aws_db_event_subscription.bar", &v), + resource.TestCheckResourceAttr( + "aws_db_event_subscription.bar", "enabled", "true"), + resource.TestCheckResourceAttr( + "aws_db_event_subscription.bar", "source_type", "db-instance"), + resource.TestCheckResourceAttr( + "aws_db_event_subscription.bar", "name", "tf-acc-test-rds-event-subs"), + resource.TestCheckResourceAttr( + "aws_db_event_subscription.bar", "tags.Name", "name"), + ), + }, + resource.TestStep{ + Config: testAccAWSDBEventSubscriptionConfigUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSDBEventSubscriptionExists("aws_db_event_subscription.bar", &v), + resource.TestCheckResourceAttr( + "aws_db_event_subscription.bar", "enabled", "false"), + resource.TestCheckResourceAttr( + "aws_db_event_subscription.bar", "source_type", "db-parameter-group"), + resource.TestCheckResourceAttr( + "aws_db_event_subscription.bar", "tags.Name", "new-name"), + ), + }, + }, + }) +} + +func testAccCheckAWSDBEventSubscriptionExists(n string, v *rds.EventSubscription) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No RDS Event Subscription is set") + } + + conn := testAccProvider.Meta().(*AWSClient).rdsconn + + opts := rds.DescribeEventSubscriptionsInput{ + SubscriptionName: aws.String(rs.Primary.ID), + } + + resp, err := conn.DescribeEventSubscriptions(&opts) + + if err != nil { + return err + } + + if len(resp.EventSubscriptionsList) != 1 || + *resp.EventSubscriptionsList[0].CustSubscriptionId != rs.Primary.ID { + return fmt.Errorf("RDS Event Subscription not found") + } + + *v = *resp.EventSubscriptionsList[0] + return nil + } +} + +func testAccCheckAWSDBEventSubscriptionDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).rdsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_db_event_subscription" { + continue + } + + var err error + resp, err := conn.DescribeEventSubscriptions( + &rds.DescribeEventSubscriptionsInput{ + SubscriptionName: aws.String(rs.Primary.ID), + }) + + if ae, ok := err.(awserr.Error); ok && ae.Code() == "SubscriptionNotFound" { + continue + } + + if err == nil { + if len(resp.EventSubscriptionsList) != 0 && + *resp.EventSubscriptionsList[0].CustSubscriptionId == rs.Primary.ID { + return fmt.Errorf("Event Subscription still exists") + } + } + + // Verify the error + newerr, ok := err.(awserr.Error) + if !ok { + return err + } + if newerr.Code() != "SubscriptionNotFound" { + return err + } + } + + return nil +} + +var testAccAWSDBEventSubscriptionConfig = ` +resource "aws_sns_topic" "aws_sns_topic" { + name = "tf-acc-test-rds-event-subs-sns-topic" +} + +resource "aws_db_event_subscription" "bar" { + name = "tf-acc-test-rds-event-subs" + sns_topic = "${aws_sns_topic.aws_sns_topic.arn}" + source_type = "db-instance" + event_categories = [ + "availability", + "backup", + "creation", + "deletion", + "maintenance" + ] + tags { + Name = "name" + } +} +` + +var testAccAWSDBEventSubscriptionConfigUpdate = ` +resource "aws_sns_topic" "aws_sns_topic" { + name = "tf-acc-test-rds-event-subs-sns-topic" +} + +resource "aws_db_event_subscription" "bar" { + name = "tf-acc-test-rds-event-subs" + sns_topic = "${aws_sns_topic.aws_sns_topic.arn}" + enabled = false + source_type = "db-parameter-group" + event_categories = [ + "configuration change" + ] + tags { + Name = "new-name" + } +} +` diff --git a/builtin/providers/aws/validators.go b/builtin/providers/aws/validators.go index 4cb31b3e7..234fe451e 100644 --- a/builtin/providers/aws/validators.go +++ b/builtin/providers/aws/validators.go @@ -438,3 +438,16 @@ func validateS3BucketLifecycleRuleId(v interface{}, k string) (ws []string, erro } return } + +func validateDbEventSubscriptionName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "only alphanumeric characters and hyphens allowed in %q", k)) + } + if len(value) > 255 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 255 characters", k)) + } + return +} diff --git a/builtin/providers/aws/validators_test.go b/builtin/providers/aws/validators_test.go index 96b391f41..f142c8af6 100644 --- a/builtin/providers/aws/validators_test.go +++ b/builtin/providers/aws/validators_test.go @@ -520,3 +520,36 @@ func TestResourceAWSElastiCacheClusterIdValidation(t *testing.T) { } } } + +func TestValidateDbEventSubscriptionName(t *testing.T) { + validNames := []string{ + "valid-name", + "valid02-name", + "Valid-Name1", + } + for _, v := range validNames { + _, errors := validateDbEventSubscriptionName(v, "name") + if len(errors) != 0 { + t.Fatalf("%q should be a valid RDS Event Subscription Name: %q", v, errors) + } + } + + invalidNames := []string{ + "Here is a name with: colon", + "and here is another * invalid name", + "also $ invalid", + "This . is also %% invalid@!)+(", + "*", + "", + " ", + "_", + // length > 255 + strings.Repeat("W", 256), + } + for _, v := range invalidNames { + _, errors := validateDbEventSubscriptionName(v, "name") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid RDS Event Subscription Name", v) + } + } +} diff --git a/website/source/docs/providers/aws/r/db_event_subscription.html.markdown b/website/source/docs/providers/aws/r/db_event_subscription.html.markdown new file mode 100644 index 000000000..646469e43 --- /dev/null +++ b/website/source/docs/providers/aws/r/db_event_subscription.html.markdown @@ -0,0 +1,34 @@ +--- +layout: "aws" +page_title: "AWS: aws_db_event_subscription" +sidebar_current: "docs-aws-resource-db-event-subscription" +--- + +# aws\_db\_event\_subscription + +Provides a DB event subscription resource. + +## Example Usage + +``` +resource "aws_sns_topic" "default" { + name = "rds-events" +} + +resource "aws_db_event_subscription" "default" { + name = "rds-event-sub" + sns_topic = "${aws_sns_topic.default.arn}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the DB event subscription. +* `sns_topic` - (Required) The SNS topic to send events to. +* `source_ids` - (Optional) A list of identifiers of the event sources for which events will be returned. If not specified, then all sources are included in the response. If specified, a source_type must also be specified. +* `source_type` - (Optional) The type of source that will be generating the events. +* `event_categories` - (Optional) A list of event categories for a SourceType that you want to subscribe to. +* `enabled` - (Optional) A boolean flag to enable/disable the subscription. Defaults to true. +* `tags` - (Optional) A mapping of tags to assign to the resource. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index cc2693c37..7aa965519 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -539,6 +539,10 @@ aws_db_instance + > + aws_db_event_subscription + + > aws_db_parameter_group