diff --git a/aws/provider.go b/aws/provider.go index 4040c614d91..4e9c4655852 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -844,6 +844,7 @@ func Provider() *schema.Provider { "aws_s3_bucket_policy": resourceAwsS3BucketPolicy(), "aws_s3_bucket_public_access_block": resourceAwsS3BucketPublicAccessBlock(), "aws_s3_bucket_object": resourceAwsS3BucketObject(), + "aws_s3_bucket_ownership_controls": resourceAwsS3BucketOwnershipControls(), "aws_s3_bucket_notification": resourceAwsS3BucketNotification(), "aws_s3_bucket_metric": resourceAwsS3BucketMetric(), "aws_s3_bucket_inventory": resourceAwsS3BucketInventory(), diff --git a/aws/resource_aws_s3_bucket_ownership_controls.go b/aws/resource_aws_s3_bucket_ownership_controls.go new file mode 100644 index 00000000000..9c0f498a337 --- /dev/null +++ b/aws/resource_aws_s3_bucket_ownership_controls.go @@ -0,0 +1,223 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceAwsS3BucketOwnershipControls() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsS3BucketOwnershipControlsCreate, + Read: resourceAwsS3BucketOwnershipControlsRead, + Update: resourceAwsS3BucketOwnershipControlsUpdate, + Delete: resourceAwsS3BucketOwnershipControlsDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + "rule": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "object_ownership": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(s3.ObjectOwnership_Values(), false), + }, + }, + }, + }, + }, + } +} + +func resourceAwsS3BucketOwnershipControlsCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).s3conn + + bucket := d.Get("bucket").(string) + + input := &s3.PutBucketOwnershipControlsInput{ + Bucket: aws.String(bucket), + OwnershipControls: &s3.OwnershipControls{ + Rules: expandS3OwnershipControlsRules(d.Get("rule").([]interface{})), + }, + } + + _, err := conn.PutBucketOwnershipControls(input) + + if err != nil { + return fmt.Errorf("error creating S3 Bucket (%s) Ownership Controls: %w", bucket, err) + } + + d.SetId(bucket) + + return resourceAwsS3BucketOwnershipControlsRead(d, meta) +} + +func resourceAwsS3BucketOwnershipControlsRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).s3conn + + input := &s3.GetBucketOwnershipControlsInput{ + Bucket: aws.String(d.Id()), + } + + output, err := conn.GetBucketOwnershipControls(input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket) { + log.Printf("[WARN] S3 Bucket Ownership Controls (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, "OwnershipControlsNotFoundError") { + log.Printf("[WARN] S3 Bucket Ownership Controls (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading S3 Bucket (%s) Ownership Controls: %w", d.Id(), err) + } + + if output == nil { + return fmt.Errorf("error reading S3 Bucket (%s) Ownership Controls: empty response", d.Id()) + } + + d.Set("bucket", d.Id()) + + if output.OwnershipControls == nil { + d.Set("rule", nil) + } else { + if err := d.Set("rule", flattenS3OwnershipControlsRules(output.OwnershipControls.Rules)); err != nil { + return fmt.Errorf("error setting rule: %w", err) + } + } + + return nil +} + +func resourceAwsS3BucketOwnershipControlsUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).s3conn + + input := &s3.PutBucketOwnershipControlsInput{ + Bucket: aws.String(d.Id()), + OwnershipControls: &s3.OwnershipControls{ + Rules: expandS3OwnershipControlsRules(d.Get("rule").([]interface{})), + }, + } + + _, err := conn.PutBucketOwnershipControls(input) + + if err != nil { + return fmt.Errorf("error updating S3 Bucket (%s) Ownership Controls: %w", d.Id(), err) + } + + return resourceAwsS3BucketOwnershipControlsRead(d, meta) +} + +func resourceAwsS3BucketOwnershipControlsDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).s3conn + + input := &s3.DeleteBucketOwnershipControlsInput{ + Bucket: aws.String(d.Id()), + } + + _, err := conn.DeleteBucketOwnershipControls(input) + + if tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket) { + return nil + } + + if tfawserr.ErrCodeEquals(err, "OwnershipControlsNotFoundError") { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting S3 Bucket (%s) Ownership Controls: %w", d.Id(), err) + } + + return nil +} + +func expandS3OwnershipControlsRules(tfList []interface{}) []*s3.OwnershipControlsRule { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + var apiObjects []*s3.OwnershipControlsRule + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObjects = append(apiObjects, expandS3OwnershipControlsRule(tfMap)) + } + + return apiObjects +} + +func expandS3OwnershipControlsRule(tfMap map[string]interface{}) *s3.OwnershipControlsRule { + if tfMap == nil { + return nil + } + + apiObject := &s3.OwnershipControlsRule{} + + if v, ok := tfMap["object_ownership"].(string); ok && v != "" { + apiObject.ObjectOwnership = aws.String(v) + } + + return apiObject +} + +func flattenS3OwnershipControlsRules(apiObjects []*s3.OwnershipControlsRule) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenS3OwnershipControlsRule(apiObject)) + } + + return tfList +} + +func flattenS3OwnershipControlsRule(apiObject *s3.OwnershipControlsRule) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.ObjectOwnership; v != nil { + tfMap["object_ownership"] = aws.StringValue(v) + } + + return tfMap +} diff --git a/aws/resource_aws_s3_bucket_ownership_controls_test.go b/aws/resource_aws_s3_bucket_ownership_controls_test.go new file mode 100644 index 00000000000..453bf167b84 --- /dev/null +++ b/aws/resource_aws_s3_bucket_ownership_controls_test.go @@ -0,0 +1,194 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccAWSS3BucketOwnershipControls_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3_bucket_ownership_controls.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketOwnershipControlsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3BucketOwnershipControlsConfig_Rule_ObjectOwnership(rName, s3.ObjectOwnershipBucketOwnerPreferred), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketOwnershipControlsExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "bucket", rName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rule.0.object_ownership", s3.ObjectOwnershipBucketOwnerPreferred), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSS3BucketOwnershipControls_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3_bucket_ownership_controls.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketOwnershipControlsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3BucketOwnershipControlsConfig_Rule_ObjectOwnership(rName, s3.ObjectOwnershipBucketOwnerPreferred), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketOwnershipControlsExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsS3BucketOwnershipControls(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSS3BucketOwnershipControls_disappears_Bucket(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3_bucket_ownership_controls.test" + s3BucketResourceName := "aws_s3_bucket.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketOwnershipControlsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3BucketOwnershipControlsConfig_Rule_ObjectOwnership(rName, s3.ObjectOwnershipBucketOwnerPreferred), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketOwnershipControlsExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsS3Bucket(), s3BucketResourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSS3BucketOwnershipControls_Rule_ObjectOwnership(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3_bucket_ownership_controls.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketOwnershipControlsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3BucketOwnershipControlsConfig_Rule_ObjectOwnership(rName, s3.ObjectOwnershipObjectWriter), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketOwnershipControlsExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "bucket", rName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rule.0.object_ownership", s3.ObjectOwnershipObjectWriter), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSS3BucketOwnershipControlsConfig_Rule_ObjectOwnership(rName, s3.ObjectOwnershipBucketOwnerPreferred), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketOwnershipControlsExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "bucket", rName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rule.0.object_ownership", s3.ObjectOwnershipBucketOwnerPreferred), + ), + }, + }, + }) +} + +func testAccCheckAWSS3BucketOwnershipControlsDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).s3conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_s3_bucket_ownership_controls" { + continue + } + + input := &s3.GetBucketOwnershipControlsInput{ + Bucket: aws.String(rs.Primary.ID), + } + + _, err := conn.GetBucketOwnershipControls(input) + + if tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket) { + continue + } + + if tfawserr.ErrCodeEquals(err, "OwnershipControlsNotFoundError") { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("S3 Bucket Ownership Controls (%s) still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckAWSS3BucketOwnershipControlsExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no resource ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).s3conn + + input := &s3.GetBucketOwnershipControlsInput{ + Bucket: aws.String(rs.Primary.ID), + } + + _, err := conn.GetBucketOwnershipControls(input) + + if err != nil { + return err + } + + return nil + } +} + +func testAccAWSS3BucketOwnershipControlsConfig_Rule_ObjectOwnership(rName, objectOwnership string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q +} + +resource "aws_s3_bucket_ownership_controls" "test" { + bucket = aws_s3_bucket.test.bucket + + rule { + object_ownership = %[2]q + } +} +`, rName, objectOwnership) +} diff --git a/website/docs/r/s3_bucket_ownership_controls.html.markdown b/website/docs/r/s3_bucket_ownership_controls.html.markdown new file mode 100644 index 00000000000..6c02dd7d131 --- /dev/null +++ b/website/docs/r/s3_bucket_ownership_controls.html.markdown @@ -0,0 +1,58 @@ +--- +subcategory: "S3" +layout: "aws" +page_title: "AWS: aws_s3_bucket_ownership_controls" +description: |- + Manages S3 Bucket Ownership Controls. +--- + +# Resource: aws_s3_bucket_ownership_controls + +Provides a resource to manage S3 Bucket Ownership Controls. For more information, see the [S3 Developer Guide](https://docs.aws.amazon.com/AmazonS3/latest/dev/about-object-ownership.html). + +~> **NOTE:** This AWS functionality is in Preview and may change before General Availability release. Backwards compatibility is not guaranteed between Terraform AWS Provider releases. + +## Example Usage + +```hcl +resource "aws_s3_bucket" "example" { + bucket = "example" +} + +resource "aws_s3_bucket_ownership_controls" "example" { + bucket = aws_s3_bucket.example.id + + rule { + object_ownership = "BucketOwnerPreferred" + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `bucket` - (Required) The name of the bucket that you want to associate this access point with. +* `rule` - (Required) Configuration block(s) with Ownership Controls rules. Detailed below. + +### rule Configuration Block + +The following arguments are required: + +* `object_ownership` - (Optional) Object ownership. Valid values: `BucketOwnerPreferred` or `ObjectWriter` + * `BucketOwnerPreferred` - Objects uploaded to the bucket change ownership to the bucket owner if the objects are uploaded with the `bucket-owner-full-control` canned ACL. + * `ObjectWriter` - The uploading account will own the object if the object is uploaded with the `bucket-owner-full-control` canned ACL. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - S3 Bucket name. + +## Import + +S3 Bucket Ownership Controls can be imported using S3 Bucket name, e.g. + +``` +$ terraform import aws_s3_bucket_ownership_controls.example my-bucket +```