diff --git a/aws/resource_aws_s3_bucket.go b/aws/resource_aws_s3_bucket.go index 72b53631ab4..d222ebc1413 100644 --- a/aws/resource_aws_s3_bucket.go +++ b/aws/resource_aws_s3_bucket.go @@ -372,6 +372,36 @@ func resourceAwsS3Bucket() *schema.Resource { 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, + }, + }, + }, + }, }, }, }, @@ -1688,8 +1718,30 @@ func resourceAwsS3BucketReplicationConfigurationUpdate(s3conn *s3.S3, d *schema. 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) } @@ -1907,6 +1959,11 @@ func flattenAwsS3BucketReplicationConfiguration(r *s3.ReplicationConfiguration) if v.Destination.StorageClass != nil { rd["storage_class"] = *v.Destination.StorageClass } + if v.Destination.EncryptionConfiguration != nil { + if v.Destination.EncryptionConfiguration.ReplicaKmsKeyID != nil { + rd["replica_kms_key_id"] = *v.Destination.EncryptionConfiguration.ReplicaKmsKeyID + } + } t["destination"] = schema.NewSet(destinationHash, []interface{}{rd}) } @@ -1919,6 +1976,19 @@ func flattenAwsS3BucketReplicationConfiguration(r *s3.ReplicationConfiguration) if v.Status != nil { t["status"] = *v.Status } + if vssc := v.SourceSelectionCriteria; vssc != nil { + tssc := make(map[string]interface{}) + if vssc.SseKmsEncryptedObjects != nil { + tSseKms := make(map[string]interface{}) + if *vssc.SseKmsEncryptedObjects.Status == s3.SseKmsEncryptedObjectsStatusEnabled { + tSseKms["enabled"] = true + } else if *vssc.SseKmsEncryptedObjects.Status == s3.SseKmsEncryptedObjectsStatusDisabled { + tSseKms["enabled"] = false + } + tssc["sse_kms_encrypted_objects"] = schema.NewSet(sourceSseKmsObjectsHash, []interface{}{tSseKms}) + } + t["source_selection_criteria"] = schema.NewSet(sourceSelectionCriteriaHash, []interface{}{tssc}) + } rules = append(rules, t) } m["rules"] = schema.NewSet(rulesHash, rules) @@ -2097,6 +2167,9 @@ func rulesHash(v interface{}) int { if v, ok := m["destination"].(*schema.Set); ok && v.Len() > 0 { buf.WriteString(fmt.Sprintf("%d-", destinationHash(v.List()[0]))) } + if v, ok := m["source_selection_criteria"].(*schema.Set); ok && v.Len() > 0 && v.List()[0] != nil { + buf.WriteString(fmt.Sprintf("%d-", sourceSelectionCriteriaHash(v.List()[0]))) + } return hashcode.String(buf.String()) } @@ -2110,6 +2183,33 @@ func destinationHash(v interface{}) int { if v, ok := m["storage_class"]; ok { buf.WriteString(fmt.Sprintf("%s-", v.(string))) } + if v, ok := m["replica_kms_key_id"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + return hashcode.String(buf.String()) +} + +func sourceSelectionCriteriaHash(v interface{}) int { + // v is nil if empty source_selection_criteria is given. + if v == nil { + return 0 + } + var buf bytes.Buffer + m := v.(map[string]interface{}) + + if v, ok := m["sse_kms_encrypted_objects"].(*schema.Set); ok && v.Len() > 0 { + buf.WriteString(fmt.Sprintf("%d-", sourceSseKmsObjectsHash(v.List()[0]))) + } + return hashcode.String(buf.String()) +} + +func sourceSseKmsObjectsHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + + if v, ok := m["enabled"]; ok { + buf.WriteString(fmt.Sprintf("%t-", v.(bool))) + } return hashcode.String(buf.String()) } diff --git a/aws/resource_aws_s3_bucket_test.go b/aws/resource_aws_s3_bucket_test.go index 08a96da9f13..6213b4cbd05 100644 --- a/aws/resource_aws_s3_bucket_test.go +++ b/aws/resource_aws_s3_bucket_test.go @@ -790,6 +790,38 @@ func TestAccAWSS3Bucket_Replication(t *testing.T) { ), ), }, + { + Config: testAccAWSS3BucketConfigReplicationWithSseKmsEncryptedObjects(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExistsWithProviders("aws_s3_bucket.bucket", &providers), + resource.TestCheckResourceAttr("aws_s3_bucket.bucket", "replication_configuration.#", "1"), + resource.TestMatchResourceAttr("aws_s3_bucket.bucket", "replication_configuration.0.role", regexp.MustCompile(fmt.Sprintf("^arn:aws:iam::[\\d+]+:role/tf-iam-role-replication-%d", rInt))), + resource.TestCheckResourceAttr("aws_s3_bucket.bucket", "replication_configuration.0.rules.#", "1"), + testAccCheckAWSS3BucketReplicationRules( + "aws_s3_bucket.bucket", + &providers, + []*s3.ReplicationRule{ + { + ID: aws.String("foobar"), + Destination: &s3.Destination{ + Bucket: aws.String(fmt.Sprintf("arn:aws:s3:::tf-test-bucket-destination-%d", rInt)), + StorageClass: aws.String(s3.ObjectStorageClassStandard), + EncryptionConfiguration: &s3.EncryptionConfiguration{ + ReplicaKmsKeyID: aws.String("${aws_kms_key.replica.arn}"), + }, + }, + Prefix: aws.String("foo"), + Status: aws.String(s3.ReplicationRuleStatusEnabled), + SourceSelectionCriteria: &s3.SourceSelectionCriteria{ + SseKmsEncryptedObjects: &s3.SseKmsEncryptedObjects{ + Status: aws.String(s3.SseKmsEncryptedObjectsStatusEnabled), + }, + }, + }, + }, + ), + ), + }, }, }) } @@ -1247,6 +1279,16 @@ func testAccCheckAWSS3BucketLogging(n, b, p string) resource.TestCheckFunc { func testAccCheckAWSS3BucketReplicationRules(n string, providers *[]*schema.Provider, rules []*s3.ReplicationRule) resource.TestCheckFunc { return func(s *terraform.State) error { rs, _ := s.RootModule().Resources[n] + for _, rule := range rules { + if dest := rule.Destination; dest != nil { + if ec := dest.EncryptionConfiguration; ec != nil { + if ec.ReplicaKmsKeyID != nil { + key_arn := s.RootModule().Resources["aws_kms_key.replica"].Primary.Attributes["arn"] + ec.ReplicaKmsKeyID = aws.String(strings.Replace(*ec.ReplicaKmsKeyID, "${aws_kms_key.replica.arn}", key_arn, -1)) + } + } + } + } for _, provider := range *providers { // Ignore if Meta is empty, this can happen for validation providers if provider.Meta() == nil { @@ -1874,6 +1916,57 @@ resource "aws_s3_bucket" "destination" { `, randInt, randInt, randInt) } +func testAccAWSS3BucketConfigReplicationWithSseKmsEncryptedObjects(randInt int) string { + return fmt.Sprintf(testAccAWSS3BucketConfigReplicationBasic+` +resource "aws_kms_key" "replica" { + provider = "aws.euwest" + description = "TF Acceptance Test S3 repl KMS key" + deletion_window_in_days = 7 +} + +resource "aws_s3_bucket" "bucket" { + provider = "aws.uswest2" + bucket = "tf-test-bucket-%d" + acl = "private" + + versioning { + enabled = true + } + + replication_configuration { + role = "${aws_iam_role.role.arn}" + rules { + id = "foobar" + prefix = "foo" + status = "Enabled" + + destination { + bucket = "${aws_s3_bucket.destination.arn}" + storage_class = "STANDARD" + replica_kms_key_id = "${aws_kms_key.replica.arn}" + } + + source_selection_criteria { + sse_kms_encrypted_objects { + enabled = true + } + } + } + } +} + +resource "aws_s3_bucket" "destination" { + provider = "aws.euwest" + bucket = "tf-test-bucket-destination-%d" + region = "eu-west-1" + + versioning { + enabled = true + } +} +`, randInt, randInt, randInt) +} + func testAccAWSS3BucketConfigReplicationWithoutStorageClass(randInt int) string { return fmt.Sprintf(testAccAWSS3BucketConfigReplicationBasic+` resource "aws_s3_bucket" "bucket" { diff --git a/website/docs/r/s3_bucket.html.markdown b/website/docs/r/s3_bucket.html.markdown index cf7d514c26e..9f66d87f40f 100644 --- a/website/docs/r/s3_bucket.html.markdown +++ b/website/docs/r/s3_bucket.html.markdown @@ -405,6 +405,7 @@ The `rules` object supports the following: * `id` - (Optional) Unique identifier for the rule. * `destination` - (Required) Specifies the destination for the rule (documented below). +* `source_selection_criteria` - (Optional) Specifies special object selection criteria (documented below). * `prefix` - (Required) Object keyname prefix identifying one or more objects to which the rule applies. Set as an empty string to replicate the whole bucket. * `status` - (Required) The status of the rule. Either `Enabled` or `Disabled`. The rule is ignored if status is not Enabled. @@ -412,6 +413,17 @@ The `destination` object supports the following: * `bucket` - (Required) The ARN of the S3 bucket where you want Amazon S3 to store replicas of the object identified by the rule. * `storage_class` - (Optional) The class of storage used to store the object. +* `replica_kms_key_id` - (Optional) Destination KMS encryption key ID for SSE-KMS replication. Must be used in conjunction with + `sse_kms_encrypted_objects` source selection criteria. + +The `source_selection_criteria` object supports the following: + +* `sse_kms_encrypted_objects` - (Optional) Match SSE-KMS encrypted objects (documented below). If specified, `replica_kms_key_id` + in `destination` must be specified as well. + +The `sse_kms_encrypted_objects` object supports the following: + +* `enabled` - (Required) Boolean which indicates if this criteria is enabled. The `server_side_encryption_configuration` object supports the following: