diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 36ecf489a..d7812e6c1 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -340,6 +340,8 @@ func Provider() terraform.ResourceProvider { "aws_ses_receipt_filter": resourceAwsSesReceiptFilter(), "aws_ses_receipt_rule": resourceAwsSesReceiptRule(), "aws_ses_receipt_rule_set": resourceAwsSesReceiptRuleSet(), + "aws_ses_configuration_set": resourceAwsSesConfigurationSet(), + "aws_ses_event_destination": resourceAwsSesEventDestination(), "aws_s3_bucket": resourceAwsS3Bucket(), "aws_s3_bucket_policy": resourceAwsS3BucketPolicy(), "aws_s3_bucket_object": resourceAwsS3BucketObject(), diff --git a/builtin/providers/aws/resource_aws_ses_configuration_set.go b/builtin/providers/aws/resource_aws_ses_configuration_set.go new file mode 100644 index 000000000..e631b887c --- /dev/null +++ b/builtin/providers/aws/resource_aws_ses_configuration_set.go @@ -0,0 +1,110 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ses" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsSesConfigurationSet() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSesConfigurationSetCreate, + Read: resourceAwsSesConfigurationSetRead, + Delete: resourceAwsSesConfigurationSetDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsSesConfigurationSetCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sesConn + + configurationSetName := d.Get("name").(string) + + createOpts := &ses.CreateConfigurationSetInput{ + ConfigurationSet: &ses.ConfigurationSet{ + Name: aws.String(configurationSetName), + }, + } + + _, err := conn.CreateConfigurationSet(createOpts) + if err != nil { + return fmt.Errorf("Error creating SES configuration set: %s", err) + } + + d.SetId(configurationSetName) + + return resourceAwsSesConfigurationSetRead(d, meta) +} + +func resourceAwsSesConfigurationSetRead(d *schema.ResourceData, meta interface{}) error { + configurationSetExists, err := findConfigurationSet(d.Id(), nil, meta) + + if !configurationSetExists { + log.Printf("[WARN] SES Configuration Set (%s) not found", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return err + } + + d.Set("name", d.Id()) + + return nil +} + +func resourceAwsSesConfigurationSetDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sesConn + + log.Printf("[DEBUG] SES Delete Configuration Rule Set: %s", d.Id()) + _, err := conn.DeleteConfigurationSet(&ses.DeleteConfigurationSetInput{ + ConfigurationSetName: aws.String(d.Id()), + }) + + if err != nil { + return err + } + + return nil +} + +func findConfigurationSet(name string, token *string, meta interface{}) (bool, error) { + conn := meta.(*AWSClient).sesConn + + configurationSetExists := false + + listOpts := &ses.ListConfigurationSetsInput{ + NextToken: token, + } + + response, err := conn.ListConfigurationSets(listOpts) + for _, element := range response.ConfigurationSets { + if *element.Name == name { + configurationSetExists = true + } + } + + if err != nil && !configurationSetExists && response.NextToken != nil { + configurationSetExists, err = findConfigurationSet(name, response.NextToken, meta) + } + + if err != nil { + return false, err + } + + return configurationSetExists, nil +} diff --git a/builtin/providers/aws/resource_aws_ses_configuration_set_test.go b/builtin/providers/aws/resource_aws_ses_configuration_set_test.go new file mode 100644 index 000000000..7cbbe4232 --- /dev/null +++ b/builtin/providers/aws/resource_aws_ses_configuration_set_test.go @@ -0,0 +1,97 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/ses" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSSESConfigurationSet_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckSESConfigurationSetDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSSESConfigurationSetConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsSESConfigurationSetExists("aws_ses_configuration_set.test"), + ), + }, + }, + }) +} + +func testAccCheckSESConfigurationSetDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).sesConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ses_configuration_set" { + continue + } + + response, err := conn.ListConfigurationSets(&ses.ListConfigurationSetsInput{}) + if err != nil { + return err + } + + found := false + for _, element := range response.ConfigurationSets { + if *element.Name == "some-configuration-set" { + found = true + } + } + + if found { + return fmt.Errorf("The configuration set still exists") + } + + } + + return nil + +} + +func testAccCheckAwsSESConfigurationSetExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("SES configuration set not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("SES configuration set ID not set") + } + + conn := testAccProvider.Meta().(*AWSClient).sesConn + + response, err := conn.ListConfigurationSets(&ses.ListConfigurationSetsInput{}) + if err != nil { + return err + } + + found := false + for _, element := range response.ConfigurationSets { + if *element.Name == "some-configuration-set" { + found = true + } + } + + if !found { + return fmt.Errorf("The configuration set was not created") + } + + return nil + } +} + +const testAccAWSSESConfigurationSetConfig = ` +resource "aws_ses_configuration_set" "test" { + name = "some-configuration-set" +} +` diff --git a/builtin/providers/aws/resource_aws_ses_event_destination.go b/builtin/providers/aws/resource_aws_ses_event_destination.go new file mode 100644 index 000000000..2dde76e5a --- /dev/null +++ b/builtin/providers/aws/resource_aws_ses_event_destination.go @@ -0,0 +1,214 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ses" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsSesEventDestination() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSesEventDestinationCreate, + Read: resourceAwsSesEventDestinationRead, + Delete: resourceAwsSesEventDestinationDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "configuration_set_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + }, + + "matching_types": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + ForceNew: true, + Set: schema.HashString, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateMatchingTypes, + }, + }, + + "cloudwatch_destination": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"kinesis_destination"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "default_value": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "dimension_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "value_source": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateDimensionValueSource, + }, + }, + }, + }, + + "kinesis_destination": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"cloudwatch_destination"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "stream_arn": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "role_arn": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + } +} + +func resourceAwsSesEventDestinationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sesConn + + configurationSetName := d.Get("configuration_set_name").(string) + eventDestinationName := d.Get("name").(string) + enabled := d.Get("enabled").(bool) + matchingEventTypes := d.Get("matching_types").(*schema.Set).List() + + createOpts := &ses.CreateConfigurationSetEventDestinationInput{ + ConfigurationSetName: aws.String(configurationSetName), + EventDestination: &ses.EventDestination{ + Name: aws.String(eventDestinationName), + Enabled: aws.Bool(enabled), + MatchingEventTypes: expandStringList(matchingEventTypes), + }, + } + + if v, ok := d.GetOk("cloudwatch_destination"); ok { + destination := v.(*schema.Set).List() + createOpts.EventDestination.CloudWatchDestination = &ses.CloudWatchDestination{ + DimensionConfigurations: generateCloudWatchDestination(destination), + } + log.Printf("[DEBUG] Creating cloudwatch destination: %#v", destination) + } + + if v, ok := d.GetOk("kinesis_destination"); ok { + destination := v.(*schema.Set).List() + if len(destination) > 1 { + return fmt.Errorf("You can only define a single kinesis destination per record") + } + kinesis := destination[0].(map[string]interface{}) + createOpts.EventDestination.KinesisFirehoseDestination = &ses.KinesisFirehoseDestination{ + DeliveryStreamARN: aws.String(kinesis["stream_arn"].(string)), + IAMRoleARN: aws.String(kinesis["role_arn"].(string)), + } + log.Printf("[DEBUG] Creating kinesis destination: %#v", kinesis) + } + + _, err := conn.CreateConfigurationSetEventDestination(createOpts) + if err != nil { + return fmt.Errorf("Error creating SES configuration set event destination: %s", err) + } + + d.SetId(eventDestinationName) + + log.Printf("[WARN] SES DONE") + return resourceAwsSesEventDestinationRead(d, meta) +} + +func resourceAwsSesEventDestinationRead(d *schema.ResourceData, meta interface{}) error { + + return nil +} + +func resourceAwsSesEventDestinationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sesConn + + log.Printf("[DEBUG] SES Delete Configuration Set Destination: %s", d.Id()) + _, err := conn.DeleteConfigurationSetEventDestination(&ses.DeleteConfigurationSetEventDestinationInput{ + ConfigurationSetName: aws.String(d.Get("configuration_set_name").(string)), + EventDestinationName: aws.String(d.Id()), + }) + + if err != nil { + return err + } + + return nil +} + +func validateMatchingTypes(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + matchingTypes := map[string]bool{ + "send": true, + "reject": true, + "bounce": true, + "complaint": true, + "delivery": true, + } + + if !matchingTypes[value] { + errors = append(errors, fmt.Errorf("%q must be a valid matching event type value: %q", k, value)) + } + return +} + +func validateDimensionValueSource(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + matchingSource := map[string]bool{ + "messageTag": true, + "emailHeader": true, + } + + if !matchingSource[value] { + errors = append(errors, fmt.Errorf("%q must be a valid dimension value: %q", k, value)) + } + return +} + +func generateCloudWatchDestination(v []interface{}) []*ses.CloudWatchDimensionConfiguration { + + b := make([]*ses.CloudWatchDimensionConfiguration, len(v)) + + for i, vI := range v { + cloudwatch := vI.(map[string]interface{}) + b[i] = &ses.CloudWatchDimensionConfiguration{ + DefaultDimensionValue: aws.String(cloudwatch["default_value"].(string)), + DimensionName: aws.String(cloudwatch["dimension_name"].(string)), + DimensionValueSource: aws.String(cloudwatch["value_source"].(string)), + } + } + + return b +} diff --git a/builtin/providers/aws/resource_aws_ses_event_destination_test.go b/builtin/providers/aws/resource_aws_ses_event_destination_test.go new file mode 100644 index 000000000..378e2042c --- /dev/null +++ b/builtin/providers/aws/resource_aws_ses_event_destination_test.go @@ -0,0 +1,185 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/ses" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSSESEventDestination_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckSESEventDestinationDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSSESEventDestinationConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsSESEventDestinationExists("aws_ses_configuration_set.test"), + resource.TestCheckResourceAttr( + "aws_ses_event_destination.kinesis", "name", "event-destination-kinesis"), + resource.TestCheckResourceAttr( + "aws_ses_event_destination.cloudwatch", "name", "event-destination-cloudwatch"), + ), + }, + }, + }) +} + +func testAccCheckSESEventDestinationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).sesConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ses_configuration_set" { + continue + } + + response, err := conn.ListConfigurationSets(&ses.ListConfigurationSetsInput{}) + if err != nil { + return err + } + + found := false + for _, element := range response.ConfigurationSets { + if *element.Name == "some-configuration-set" { + found = true + } + } + + if found { + return fmt.Errorf("The configuration set still exists") + } + + } + + return nil + +} + +func testAccCheckAwsSESEventDestinationExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("SES event destination not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("SES event destination ID not set") + } + + conn := testAccProvider.Meta().(*AWSClient).sesConn + + response, err := conn.ListConfigurationSets(&ses.ListConfigurationSetsInput{}) + if err != nil { + return err + } + + found := false + for _, element := range response.ConfigurationSets { + if *element.Name == "some-configuration-set" { + found = true + } + } + + if !found { + return fmt.Errorf("The configuration set was not created") + } + + return nil + } +} + +const testAccAWSSESEventDestinationConfig = ` +resource "aws_s3_bucket" "bucket" { + bucket = "tf-test-bucket-format" + acl = "private" +} + +resource "aws_iam_role" "firehose_role" { + name = "firehose_test_role_test" + assume_role_policy = < **NOTE:** You can specify `"cloudwatch_destination"` or `"kinesis_destination"` but not both + +CloudWatch Destination requires the following: + +* `default_value` - (Required) The default value for the event +* `dimension_name` - (Required) The name for the dimension +* `value_source` - (Required) The source for the value. It can be either `"messageTag"` or `"emailHeader"` + +Kinesis Destination requires the following: + +* `stream_arn` - (Required) The ARN of the Kinesis Stream +* `role_arn` - (Required) The ARN of the role that has permissions to access the Kinesis Stream + diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index d21f60002..736863ee3 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -962,6 +962,14 @@ aws_ses_receipt_rule_set + > + aws_ses_configuration_set + + + > + aws_ses_event_destination + +