diff --git a/aws/provider.go b/aws/provider.go index 6aa0203b3ac..daa38337606 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -490,6 +490,7 @@ func Provider() terraform.ResourceProvider { "aws_s3_bucket": resourceAwsS3Bucket(), "aws_s3_bucket_policy": resourceAwsS3BucketPolicy(), "aws_s3_bucket_object": resourceAwsS3BucketObject(), + "aws_s3_bucket_replication": resourceAwsS3BucketReplication(), "aws_s3_bucket_notification": resourceAwsS3BucketNotification(), "aws_s3_bucket_metric": resourceAwsS3BucketMetric(), "aws_security_group": resourceAwsSecurityGroup(), diff --git a/aws/resource_aws_s3_bucket.go b/aws/resource_aws_s3_bucket.go index a7af215eb46..eee088272e0 100644 --- a/aws/resource_aws_s3_bucket.go +++ b/aws/resource_aws_s3_bucket.go @@ -994,25 +994,25 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error { } // Read the bucket replication configuration - - replicationResponse, err := retryOnAwsCode("NoSuchBucket", func() (interface{}, error) { - return s3conn.GetBucketReplication(&s3.GetBucketReplicationInput{ - Bucket: aws.String(d.Id()), + if _, ok := d.GetOk("replication_configuration"); ok { + replicationResponse, err := retryOnAwsCode("NoSuchBucket", func() (interface{}, error) { + return s3conn.GetBucketReplication(&s3.GetBucketReplicationInput{ + Bucket: aws.String(d.Id()), + }) }) - }) - if err != nil { - if awsError, ok := err.(awserr.RequestFailure); ok && awsError.StatusCode() != 404 { - return err + if err != nil { + if awsError, ok := err.(awserr.RequestFailure); ok && awsError.StatusCode() != 404 { + return err + } } - } - replication := replicationResponse.(*s3.GetBucketReplicationOutput) + replication := replicationResponse.(*s3.GetBucketReplicationOutput) - log.Printf("[DEBUG] S3 Bucket: %s, read replication configuration: %v", d.Id(), replication) - if err := d.Set("replication_configuration", flattenAwsS3BucketReplicationConfiguration(replication.ReplicationConfiguration)); err != nil { - log.Printf("[DEBUG] Error setting replication configuration: %s", err) - return err + log.Printf("[DEBUG] S3 Bucket: %s, read replication configuration: %v", d.Id(), replication) + if err := d.Set("replication_configuration", flattenAwsS3BucketReplicationConfiguration(replication.ReplicationConfiguration)); err != nil { + log.Printf("[DEBUG] Error setting replication configuration: %s", err) + return err + } } - // Read the bucket server side encryption configuration encryptionResponse, err := retryOnAwsCode("NoSuchBucket", func() (interface{}, error) { @@ -1037,7 +1037,6 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error { } } } - // Add the region as an attribute locationResponse, err := retryOnAwsCode("NoSuchBucket", func() (interface{}, error) { @@ -1691,60 +1690,8 @@ func resourceAwsS3BucketReplicationConfigurationUpdate(s3conn *s3.S3, d *schema. c := replicationConfiguration[0].(map[string]interface{}) - rc := &s3.ReplicationConfiguration{} - if val, ok := c["role"]; ok { - rc.Role = aws.String(val.(string)) - } - - rcRules := c["rules"].(*schema.Set).List() - rules := []*s3.ReplicationRule{} - for _, v := range rcRules { - rr := v.(map[string]interface{}) - rcRule := &s3.ReplicationRule{ - Prefix: aws.String(rr["prefix"].(string)), - Status: aws.String(rr["status"].(string)), - } - - if rrid, ok := rr["id"]; ok { - rcRule.ID = aws.String(rrid.(string)) - } - - ruleDestination := &s3.Destination{} - if dest, ok := rr["destination"].(*schema.Set); ok && dest.Len() > 0 { - bd := dest.List()[0].(map[string]interface{}) - ruleDestination.Bucket = aws.String(bd["bucket"].(string)) - - if storageClass, ok := bd["storage_class"]; ok && storageClass != "" { - ruleDestination.StorageClass = aws.String(storageClass.(string)) - } - - if replicaKmsKeyId, ok := bd["replica_kms_key_id"]; ok && replicaKmsKeyId != "" { - ruleDestination.EncryptionConfiguration = &s3.EncryptionConfiguration{ - ReplicaKmsKeyID: aws.String(replicaKmsKeyId.(string)), - } - } - } - rcRule.Destination = ruleDestination - - if ssc, ok := rr["source_selection_criteria"].(*schema.Set); ok && ssc.Len() > 0 { - sscValues := ssc.List()[0].(map[string]interface{}) - ruleSsc := &s3.SourceSelectionCriteria{} - if sseKms, ok := sscValues["sse_kms_encrypted_objects"].(*schema.Set); ok && sseKms.Len() > 0 { - sseKmsValues := sseKms.List()[0].(map[string]interface{}) - sseKmsEncryptedObjects := &s3.SseKmsEncryptedObjects{} - if sseKmsValues["enabled"].(bool) { - sseKmsEncryptedObjects.Status = aws.String(s3.SseKmsEncryptedObjectsStatusEnabled) - } else { - sseKmsEncryptedObjects.Status = aws.String(s3.SseKmsEncryptedObjectsStatusDisabled) - } - ruleSsc.SseKmsEncryptedObjects = sseKmsEncryptedObjects - } - rcRule.SourceSelectionCriteria = ruleSsc - } - rules = append(rules, rcRule) - } + rc := buildAwsS3BucketReplicationConfiguration(c) - rc.Rules = rules i := &s3.PutBucketReplicationInput{ Bucket: aws.String(bucket), ReplicationConfiguration: rc, @@ -2002,6 +1949,64 @@ func flattenAwsS3BucketReplicationConfiguration(r *s3.ReplicationConfiguration) return replication_configuration } +func buildAwsS3BucketReplicationConfiguration(c map[string]interface{}) *s3.ReplicationConfiguration { + rc := &s3.ReplicationConfiguration{} + if val, ok := c["role"]; ok { + rc.Role = aws.String(val.(string)) + } + + rcRules := c["rules"].(*schema.Set).List() + rules := []*s3.ReplicationRule{} + for _, v := range rcRules { + rr := v.(map[string]interface{}) + rcRule := &s3.ReplicationRule{ + Prefix: aws.String(rr["prefix"].(string)), + Status: aws.String(rr["status"].(string)), + } + + if rrid, ok := rr["id"]; ok { + rcRule.ID = aws.String(rrid.(string)) + } + + ruleDestination := &s3.Destination{} + if dest, ok := rr["destination"].(*schema.Set); ok && dest.Len() > 0 { + bd := dest.List()[0].(map[string]interface{}) + ruleDestination.Bucket = aws.String(bd["bucket"].(string)) + + if storageClass, ok := bd["storage_class"]; ok && storageClass != "" { + ruleDestination.StorageClass = aws.String(storageClass.(string)) + } + + if replicaKmsKeyId, ok := bd["replica_kms_key_id"]; ok && replicaKmsKeyId != "" { + ruleDestination.EncryptionConfiguration = &s3.EncryptionConfiguration{ + ReplicaKmsKeyID: aws.String(replicaKmsKeyId.(string)), + } + } + } + rcRule.Destination = ruleDestination + + if ssc, ok := rr["source_selection_criteria"].(*schema.Set); ok && ssc.Len() > 0 { + sscValues := ssc.List()[0].(map[string]interface{}) + ruleSsc := &s3.SourceSelectionCriteria{} + if sseKms, ok := sscValues["sse_kms_encrypted_objects"].(*schema.Set); ok && sseKms.Len() > 0 { + sseKmsValues := sseKms.List()[0].(map[string]interface{}) + sseKmsEncryptedObjects := &s3.SseKmsEncryptedObjects{} + if sseKmsValues["enabled"].(bool) { + sseKmsEncryptedObjects.Status = aws.String(s3.SseKmsEncryptedObjectsStatusEnabled) + } else { + sseKmsEncryptedObjects.Status = aws.String(s3.SseKmsEncryptedObjectsStatusDisabled) + } + ruleSsc.SseKmsEncryptedObjects = sseKmsEncryptedObjects + } + rcRule.SourceSelectionCriteria = ruleSsc + } + rules = append(rules, rcRule) + } + + rc.Rules = rules + return rc +} + func normalizeRoutingRules(w []*s3.RoutingRule) (string, error) { withNulls, err := json.Marshal(w) if err != nil { diff --git a/aws/resource_aws_s3_bucket_replication.go b/aws/resource_aws_s3_bucket_replication.go new file mode 100644 index 00000000000..d0dad1a7b59 --- /dev/null +++ b/aws/resource_aws_s3_bucket_replication.go @@ -0,0 +1,192 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsS3BucketReplication() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsS3BucketReplicationPut, + Read: resourceAwsS3BucketReplicationRead, + Update: resourceAwsS3BucketReplicationPut, + Delete: resourceAwsS3BucketReplicationDelete, + + Schema: map[string]*schema.Schema{ + "bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "replication_configuration": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "role": { + Type: schema.TypeString, + Required: true, + }, + "rules": { + Type: schema.TypeSet, + Required: true, + Set: rulesHash, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateS3BucketReplicationRuleId, + }, + "destination": { + Type: schema.TypeSet, + MaxItems: 1, + MinItems: 1, + Required: true, + Set: destinationHash, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bucket": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "storage_class": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateS3BucketReplicationDestinationStorageClass, + }, + "replica_kms_key_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "source_selection_criteria": { + Type: schema.TypeSet, + Optional: true, + MinItems: 1, + MaxItems: 1, + Set: sourceSelectionCriteriaHash, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "sse_kms_encrypted_objects": { + Type: schema.TypeSet, + Optional: true, + MinItems: 1, + MaxItems: 1, + Set: sourceSseKmsObjectsHash, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "prefix": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateS3BucketReplicationRulePrefix, + }, + "status": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateS3BucketReplicationRuleStatus, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func resourceAwsS3BucketReplicationPut(d *schema.ResourceData, meta interface{}) error { + s3conn := meta.(*AWSClient).s3conn + + bucket := d.Get("bucket").(string) + + replicationConfiguration := d.Get("replication_configuration").([]interface{}) + c := replicationConfiguration[0].(map[string]interface{}) + + rc := buildAwsS3BucketReplicationConfiguration(c) + + params := &s3.PutBucketReplicationInput{ + Bucket: aws.String(bucket), + ReplicationConfiguration: rc, + } + + log.Printf("[DEBUG] S3 put bucket replication configuration: %#v", params) + + _, err := s3conn.PutBucketReplication(params) + + if err != nil { + return fmt.Errorf("Error putting S3 replication configuration: %s", err) + } + + d.SetId(bucket) + return resourceAwsS3BucketReplicationRead(d, meta) +} + +func resourceAwsS3BucketReplicationRead(d *schema.ResourceData, meta interface{}) error { + s3conn := meta.(*AWSClient).s3conn + + log.Printf("[DEBUG] S3 bucket replication, reading for bucket: %s", d.Id()) + + replication, err := s3conn.GetBucketReplication(&s3.GetBucketReplicationInput{ + Bucket: aws.String(d.Id()), + }) + + if err != nil { + if awsError, ok := err.(awserr.RequestFailure); ok && awsError.StatusCode() != 404 { + return err + } + } + + log.Printf("[DEBUG] S3 bucket: %s, read replication configuration: %v", d.Id(), replication) + + if r := replication.ReplicationConfiguration; r != nil { + if err := d.Set("replication_configuration", flattenAwsS3BucketReplicationConfiguration(replication.ReplicationConfiguration)); err != nil { + log.Printf("[DEBUG] Error setting replication configuration: %s", err) + return err + } + } + + return nil +} + +func resourceAwsS3BucketReplicationDelete(d *schema.ResourceData, meta interface{}) error { + s3conn := meta.(*AWSClient).s3conn + + bucket := d.Get("bucket").(string) + + log.Printf("[DEBUG] S3 bucket: %s, delete replication configuration", bucket) + + _, err := s3conn.DeleteBucketReplication(&s3.DeleteBucketReplicationInput{ + Bucket: aws.String(bucket), + }) + + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoSuchBucket" { + return nil + } + return fmt.Errorf("Error deleting S3 replication configuration: %s", err) + } + + d.SetId("") + return nil +} diff --git a/aws/resource_aws_s3_bucket_replication_test.go b/aws/resource_aws_s3_bucket_replication_test.go new file mode 100644 index 00000000000..6cdc94ea7a5 --- /dev/null +++ b/aws/resource_aws_s3_bucket_replication_test.go @@ -0,0 +1,232 @@ +package aws + +import ( + "fmt" + "strings" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSS3BucketReplication_basic(t *testing.T) { + name := fmt.Sprintf("tf-test-bucket-%d", acctest.RandInt()) + + // record the initialized providers so that we can use them to check for the instances in each region + var providers []*schema.Provider + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories(&providers), + CheckDestroy: testAccCheckWithProviders(testAccCheckAWSS3BucketDestroyWithProvider, &providers), + Steps: []resource.TestStep{ + { + Config: testAccAWSS3BucketReplicationConfigBase(name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("aws_s3_bucket_replication.replication", "replication_configuration.#", "1"), + resource.TestCheckResourceAttr("aws_s3_bucket_replication.replication", "replication_configuration.0.rules.#", "1"), + testAccCheckAWSS3BucketHasReplicationWithProvider("aws_s3_bucket.main", name+"-replica", testAccAwsRegionProviderFunc("us-west-2", &providers)), + ), + }, { + Config: testAccAWSS3BucketReplicationConfigUpdated(name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("aws_s3_bucket_replication.replication", "replication_configuration.#", "1"), + resource.TestCheckResourceAttr("aws_s3_bucket_replication.replication", "replication_configuration.0.rules.#", "1"), + testAccCheckAWSS3BucketHasReplicationWithProvider("aws_s3_bucket.main", name+"-replica-2", testAccAwsRegionProviderFunc("us-west-2", &providers)), + ), + }, + }, + }) +} + +func testAccCheckAWSS3BucketHasReplicationWithProvider(n, b string, providerF func() *schema.Provider) 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 S3 Bucket ID is set") + } + + provider := providerF() + conn := provider.Meta().(*AWSClient).s3conn + rc, err := conn.GetBucketReplication(&s3.GetBucketReplicationInput{ + Bucket: aws.String(rs.Primary.ID), + }) + + if err != nil { + return fmt.Errorf("GetBucketReplication error: %v", err) + } + + destinationBucket := *rc.ReplicationConfiguration.Rules[0].Destination.Bucket + if strings.Contains(destinationBucket, b) { + return nil + } + return fmt.Errorf("No replication bucket found") + } +} + +func testAccAWSS3BucketReplicationConfig_basePrefix(bucketName string) string { + return fmt.Sprintf(` +provider "aws" { + alias = "usw2" + region = "us-west-2" +} + +provider "aws" { + alias = "use2" + region = "us-east-2" +} + +resource "aws_s3_bucket" "main" { + provider = "aws.usw2" + bucket = "%s" + region = "us-west-2" + + versioning { + enabled = true + } +} + +resource "aws_s3_bucket" "destination" { + provider = "aws.use2" + region = "us-east-2" + bucket = "%s-replica" + + versioning { + enabled = true + } +} + +resource "aws_s3_bucket" "destination2" { + provider = "aws.use2" + region = "us-east-2" + bucket = "%s-replica-2" + + versioning { + enabled = true + } +} + +resource "aws_iam_role" "replication" { + name = "%s-role" + + assume_role_policy = <