diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 494e18df435..29b29d9f605 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1567,6 +1567,7 @@ func Provider() *schema.Provider { "aws_s3_bucket_policy": s3.ResourceBucketPolicy(), "aws_s3_bucket_public_access_block": s3.ResourceBucketPublicAccessBlock(), "aws_s3_bucket_replication_configuration": s3.ResourceBucketReplicationConfiguration(), + "aws_s3_bucket_request_payment_configuration": s3.ResourceBucketRequestPaymentConfiguration(), "aws_s3_object_copy": s3.ResourceObjectCopy(), "aws_s3_access_point": s3control.ResourceAccessPoint(), diff --git a/internal/service/s3/bucket_request_payment_configuration.go b/internal/service/s3/bucket_request_payment_configuration.go new file mode 100644 index 00000000000..ac6bfefb2b4 --- /dev/null +++ b/internal/service/s3/bucket_request_payment_configuration.go @@ -0,0 +1,205 @@ +package s3 + +import ( + "context" + "fmt" + "log" + "strings" + + "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/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func ResourceBucketRequestPaymentConfiguration() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceBucketRequestPaymentConfigurationCreate, + ReadContext: resourceBucketRequestPaymentConfigurationRead, + UpdateContext: resourceBucketRequestPaymentConfigurationUpdate, + DeleteContext: resourceBucketRequestPaymentConfigurationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 63), + }, + "expected_bucket_owner": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: verify.ValidAccountID, + }, + "payer": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(s3.Payer_Values(), false), + }, + }, + } +} + +func resourceBucketRequestPaymentConfigurationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket := d.Get("bucket").(string) + expectedBucketOwner := d.Get("expected_bucket_owner").(string) + + input := &s3.PutBucketRequestPaymentInput{ + Bucket: aws.String(bucket), + RequestPaymentConfiguration: &s3.RequestPaymentConfiguration{ + Payer: aws.String(d.Get("payer").(string)), + }, + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + _, err := verify.RetryOnAWSCode(s3.ErrCodeNoSuchBucket, func() (interface{}, error) { + return conn.PutBucketRequestPaymentWithContext(ctx, input) + }) + + if err != nil { + return diag.FromErr(fmt.Errorf("error creating S3 bucket (%s) request payment configuration: %w", bucket, err)) + } + + d.SetId(resourceBucketRequestPaymentConfigurationCreateResourceID(bucket, expectedBucketOwner)) + + return resourceBucketRequestPaymentConfigurationRead(ctx, d, meta) +} + +func resourceBucketRequestPaymentConfigurationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket, expectedBucketOwner, err := resourceBucketRequestPaymentConfigurationParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + input := &s3.GetBucketRequestPaymentInput{ + Bucket: aws.String(bucket), + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + output, err := conn.GetBucketRequestPaymentWithContext(ctx, input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket) { + log.Printf("[WARN] S3 Bucket Request Payment Configuration (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if output == nil { + return diag.FromErr(fmt.Errorf("error reading S3 bucket request payment configuration (%s): empty output", d.Id())) + } + + d.Set("bucket", bucket) + d.Set("expected_bucket_owner", expectedBucketOwner) + d.Set("payer", output.Payer) + + return nil +} + +func resourceBucketRequestPaymentConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket, expectedBucketOwner, err := resourceBucketRequestPaymentConfigurationParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + input := &s3.PutBucketRequestPaymentInput{ + Bucket: aws.String(bucket), + RequestPaymentConfiguration: &s3.RequestPaymentConfiguration{ + Payer: aws.String(d.Get("payer").(string)), + }, + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + _, err = conn.PutBucketRequestPaymentWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error updating S3 bucket request payment configuration (%s): %w", d.Id(), err)) + } + + return resourceBucketRequestPaymentConfigurationRead(ctx, d, meta) +} + +func resourceBucketRequestPaymentConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket, expectedBucketOwner, err := resourceBucketRequestPaymentConfigurationParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + input := &s3.PutBucketRequestPaymentInput{ + Bucket: aws.String(bucket), + RequestPaymentConfiguration: &s3.RequestPaymentConfiguration{ + // To remove a configuration, it is equivalent to disabling + // "Requester Pays" in the console; thus, we reset "Payer" back to "BucketOwner" + Payer: aws.String(s3.PayerBucketOwner), + }, + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + _, err = conn.PutBucketRequestPaymentWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket) { + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error deleting S3 bucket request payment configuration (%s): %w", d.Id(), err)) + } + + return nil +} + +func resourceBucketRequestPaymentConfigurationCreateResourceID(bucket, expectedBucketOwner string) string { + if bucket == "" { + return expectedBucketOwner + } + + if expectedBucketOwner == "" { + return bucket + } + + parts := []string{bucket, expectedBucketOwner} + id := strings.Join(parts, ",") + + return id +} + +func resourceBucketRequestPaymentConfigurationParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, ",") + + if len(parts) == 1 && parts[0] != "" { + return parts[0], "", nil + } + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected BUCKET or BUCKET,EXPECTED_BUCKET_OWNER", id) +} diff --git a/internal/service/s3/bucket_request_payment_configuration_test.go b/internal/service/s3/bucket_request_payment_configuration_test.go new file mode 100644 index 00000000000..887018399cd --- /dev/null +++ b/internal/service/s3/bucket_request_payment_configuration_test.go @@ -0,0 +1,186 @@ +package s3_test + +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" + sdkacctest "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" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" +) + +func TestAccS3BucketRequestPaymentConfiguration_Basic_BucketOwner(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_request_payment_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketRequestPaymentConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketRequestPaymentConfigurationBasicConfig(rName, s3.PayerBucketOwner), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketRequestPaymentConfigurationExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "aws_s3_bucket.test", "id"), + resource.TestCheckResourceAttr(resourceName, "payer", s3.PayerBucketOwner), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3BucketRequestPaymentConfiguration_Basic_Requester(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_request_payment_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketRequestPaymentConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketRequestPaymentConfigurationBasicConfig(rName, s3.PayerRequester), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketRequestPaymentConfigurationExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "aws_s3_bucket.test", "id"), + resource.TestCheckResourceAttr(resourceName, "payer", s3.PayerRequester), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3BucketRequestPaymentConfiguration_update(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_request_payment_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketRequestPaymentConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketRequestPaymentConfigurationBasicConfig(rName, s3.PayerRequester), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketRequestPaymentConfigurationExists(resourceName), + ), + }, + { + Config: testAccBucketRequestPaymentConfigurationBasicConfig(rName, s3.PayerBucketOwner), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketRequestPaymentConfigurationExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBucketRequestPaymentConfigurationBasicConfig(rName, s3.PayerRequester), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketRequestPaymentConfigurationExists(resourceName), + ), + }, + }, + }) +} + +func testAccCheckBucketRequestPaymentConfigurationDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).S3Conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_s3_bucket_request_payment_configuration" { + continue + } + + input := &s3.GetBucketRequestPaymentInput{ + Bucket: aws.String(rs.Primary.ID), + } + + output, err := conn.GetBucketRequestPayment(input) + + if tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket) { + continue + } + + if err != nil { + return fmt.Errorf("error getting S3 bucket request payment configuration (%s): %w", rs.Primary.ID, err) + } + + if output != nil && aws.StringValue(output.Payer) != s3.PayerBucketOwner { + return fmt.Errorf("S3 bucket request payment configuration (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckBucketRequestPaymentConfigurationExists(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("Resource (%s) ID not set", resourceName) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).S3Conn + + input := &s3.GetBucketRequestPaymentInput{ + Bucket: aws.String(rs.Primary.ID), + } + + output, err := conn.GetBucketRequestPayment(input) + + if err != nil { + return fmt.Errorf("error getting S3 bucket request payment configuration (%s): %w", rs.Primary.ID, err) + } + + if output == nil { + return fmt.Errorf("S3 Bucket request payment configuration (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccBucketRequestPaymentConfigurationBasicConfig(rName, payer string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + + lifecycle { + ignore_changes = [ + request_payer + ] + } +} + +resource "aws_s3_bucket_request_payment_configuration" "test" { + bucket = aws_s3_bucket.test.id + payer = %[2]q +} +`, rName, payer) +} diff --git a/website/docs/r/s3_bucket_request_payment_configuration.html.markdown b/website/docs/r/s3_bucket_request_payment_configuration.html.markdown new file mode 100644 index 00000000000..4d522dcdc23 --- /dev/null +++ b/website/docs/r/s3_bucket_request_payment_configuration.html.markdown @@ -0,0 +1,50 @@ +--- +subcategory: "S3" +layout: "aws" +page_title: "AWS: aws_s3_bucket_request_payment_configuration" +description: |- + Provides an S3 bucket request payment configuration resource. +--- + +# Resource: aws_s3_bucket_request_payment_configuration + +Provides an S3 bucket request payment configuration resource. For more information, see [Requester Pays Buckets](https://docs.aws.amazon.com/AmazonS3/latest/dev/RequesterPaysBuckets.html). + +~> **NOTE:** Destroying an `aws_s3_bucket_request_payment_configuration` resource resets the bucket's `payer` to the S3 default: the bucket owner. + +## Example Usage + +```terraform +resource "aws_s3_bucket_request_payment_configuration" "example" { + bucket = aws_s3_bucket.example.bucket + payer = "Requester" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `bucket` - (Required, Forces new resource) The name of the bucket. +* `expected_bucket_owner` - (Optional, Forces new resource) The account ID of the expected bucket owner. +* `payer` - (Required) Specifies who pays for the download and request fees. Valid values: `BucketOwner`, `Requester`. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The `bucket` or `bucket` and `expected_bucket_owner` separated by a comma (`,`) if the latter is provided. + +## Import + +S3 bucket request payment configuration can be imported using the `bucket` e.g., + +``` +$ terraform import aws_s3_bucket_request_payment_configuration.example bucket-name +``` + +In addition, S3 bucket request payment configuration can be imported using the `bucket` and `expected_bucket_owner` separated by a comma (`,`) e.g., + +``` +$ terraform import aws_s3_bucket_request_payment_configuration.example bucket-name,123456789012 +```