From 762e152961784ad1aed99fdeafb3bb91ad0baa04 Mon Sep 17 00:00:00 2001 From: Angie Pinilla Date: Tue, 18 Jan 2022 15:56:22 -0500 Subject: [PATCH 1/5] r/s3_bucket_object_lock_configuration: new resource --- internal/provider/provider.go | 1 + internal/service/s3/bucket.go | 4 +- .../s3/bucket_object_lock_configuration.go | 363 ++++++++++++++++++ .../bucket_object_lock_configuration_test.go | 222 +++++++++++ internal/service/s3/errors.go | 1 + ...et_object_lock_configuration.html.markdown | 79 ++++ 6 files changed, 668 insertions(+), 2 deletions(-) create mode 100644 internal/service/s3/bucket_object_lock_configuration.go create mode 100644 internal/service/s3/bucket_object_lock_configuration_test.go create mode 100644 website/docs/r/s3_bucket_object_lock_configuration.html.markdown diff --git a/internal/provider/provider.go b/internal/provider/provider.go index da8a8567ac6..b15f7d98297 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1627,6 +1627,7 @@ func Provider() *schema.Provider { "aws_s3_bucket_inventory": s3.ResourceBucketInventory(), "aws_s3_bucket_metric": s3.ResourceBucketMetric(), "aws_s3_bucket_notification": s3.ResourceBucketNotification(), + "aws_s3_bucket_object_lock_configuration": s3.ResourceBucketObjectLockConfiguration(), "aws_s3_bucket_ownership_controls": s3.ResourceBucketOwnershipControls(), "aws_s3_bucket_policy": s3.ResourceBucketPolicy(), "aws_s3_bucket_public_access_block": s3.ResourceBucketPublicAccessBlock(), diff --git a/internal/service/s3/bucket.go b/internal/service/s3/bucket.go index 4c5f8836b78..0ef0194b71b 100644 --- a/internal/service/s3/bucket.go +++ b/internal/service/s3/bucket.go @@ -842,7 +842,7 @@ func resourceBucketUpdate(d *schema.ResourceData, meta interface{}) error { } if d.HasChange("object_lock_configuration") { - if err := resourceObjectLockConfigurationUpdate(conn, d); err != nil { + if err := resourceBucketInternalObjectLockConfigurationUpdate(conn, d); err != nil { return err } } @@ -2014,7 +2014,7 @@ func resourceBucketServerSideEncryptionConfigurationUpdate(conn *s3.S3, d *schem return nil } -func resourceObjectLockConfigurationUpdate(conn *s3.S3, d *schema.ResourceData) error { +func resourceBucketInternalObjectLockConfigurationUpdate(conn *s3.S3, d *schema.ResourceData) error { // S3 Object Lock configuration cannot be deleted, only updated. req := &s3.PutObjectLockConfigurationInput{ Bucket: aws.String(d.Get("bucket").(string)), diff --git a/internal/service/s3/bucket_object_lock_configuration.go b/internal/service/s3/bucket_object_lock_configuration.go new file mode 100644 index 00000000000..0e8b8ac835b --- /dev/null +++ b/internal/service/s3/bucket_object_lock_configuration.go @@ -0,0 +1,363 @@ +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 ResourceBucketObjectLockConfiguration() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceBucketObjectLockConfigurationCreate, + ReadContext: resourceBucketObjectLockConfigurationRead, + UpdateContext: resourceBucketObjectLockConfigurationUpdate, + DeleteContext: resourceBucketObjectLockConfigurationDelete, + 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, + }, + "object_lock_enabled": { + Type: schema.TypeString, + Optional: true, + Default: s3.ObjectLockEnabledEnabled, + ForceNew: true, + ValidateFunc: validation.StringInSlice(s3.ObjectLockEnabled_Values(), false), + }, + "rule": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "default_retention": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "days": { + Type: schema.TypeInt, + Optional: true, + ConflictsWith: []string{"rule.0.default_retention.0.years"}, + }, + "mode": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(s3.ObjectLockRetentionMode_Values(), false), + }, + "years": { + Type: schema.TypeInt, + Optional: true, + ConflictsWith: []string{"rule.0.default_retention.0.days"}, + }, + }, + }, + }, + }, + }, + }, + "token": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceBucketObjectLockConfigurationCreate(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.PutObjectLockConfigurationInput{ + Bucket: aws.String(bucket), + ObjectLockConfiguration: &s3.ObjectLockConfiguration{ + // ObjectLockEnabled is required by the API, even if configured directly on the S3 bucket + // during creation, else a MalformedXML error will be returned. + ObjectLockEnabled: aws.String(d.Get("object_lock_enabled").(string)), + Rule: expandBucketObjectLockConfigurationRule(d.Get("rule").([]interface{})), + }, + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + if v, ok := d.GetOk("request_payer"); ok { + input.RequestPayer = aws.String(v.(string)) + } + + // For existing buckets + if v, ok := d.GetOk("token"); ok { + input.Token = aws.String(v.(string)) + } + + _, err := verify.RetryOnAWSCode(s3.ErrCodeNoSuchBucket, func() (interface{}, error) { + return conn.PutObjectLockConfigurationWithContext(ctx, input) + }) + + if err != nil { + return diag.FromErr(fmt.Errorf("error creating S3 bucket (%s) Object Lock configuration: %w", bucket, err)) + } + + d.SetId(resourceBucketObjectLockConfigurationCreateResourceID(bucket, expectedBucketOwner)) + + return resourceBucketObjectLockConfigurationRead(ctx, d, meta) +} + +func resourceBucketObjectLockConfigurationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket, expectedBucketOwner, err := resourceBucketObjectLockConfigurationParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + input := &s3.GetObjectLockConfigurationInput{ + Bucket: aws.String(bucket), + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + output, err := conn.GetObjectLockConfigurationWithContext(ctx, input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket, ErrCodeObjectLockConfigurationNotFound) { + log.Printf("[WARN] S3 Bucket Object Lock Configuration (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading S3 bucket Object Lock configuration (%s): %w", d.Id(), err)) + } + + if output == nil || output.ObjectLockConfiguration == nil { + if d.IsNewResource() { + return diag.FromErr(fmt.Errorf("error reading S3 bucket Object Lock configuration (%s): empty output", d.Id())) + } + log.Printf("[WARN] S3 Bucket Object Lock Configuration (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + objLockConfig := output.ObjectLockConfiguration + + d.Set("bucket", bucket) + d.Set("expected_bucket_owner", expectedBucketOwner) + d.Set("object_lock_enabled", objLockConfig.ObjectLockEnabled) + + if err := d.Set("rule", flattenBucketObjectLockConfigurationRule(objLockConfig.Rule)); err != nil { + return diag.FromErr(fmt.Errorf("error setting rule: %w", err)) + } + + return nil +} + +func resourceBucketObjectLockConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket, expectedBucketOwner, err := resourceBucketObjectLockConfigurationParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + input := &s3.PutObjectLockConfigurationInput{ + Bucket: aws.String(bucket), + ObjectLockConfiguration: &s3.ObjectLockConfiguration{ + // ObjectLockEnabled is required by the API, even if configured directly on the S3 bucket + // during creation, else a MalformedXML error will be returned. + ObjectLockEnabled: aws.String(d.Get("object_lock_enabled").(string)), + Rule: expandBucketObjectLockConfigurationRule(d.Get("rule").([]interface{})), + }, + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + if v, ok := d.GetOk("request_payer"); ok { + input.RequestPayer = aws.String(v.(string)) + } + + if v, ok := d.GetOk("token"); ok { + input.Token = aws.String(v.(string)) + } + + _, err = conn.PutObjectLockConfigurationWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error updating S3 bucket Object Lock configuration (%s): %w", d.Id(), err)) + } + + return resourceBucketObjectLockConfigurationRead(ctx, d, meta) +} + +func resourceBucketObjectLockConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket, expectedBucketOwner, err := resourceBucketObjectLockConfigurationParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + input := &s3.PutObjectLockConfigurationInput{ + Bucket: aws.String(bucket), + ObjectLockConfiguration: &s3.ObjectLockConfiguration{ + // ObjectLockEnabled is required by the API, even if configured directly on the S3 bucket + // during creation, else a MalformedXML error will be returned. + ObjectLockEnabled: aws.String(d.Get("object_lock_enabled").(string)), + }, + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + _, err = conn.PutObjectLockConfigurationWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket, ErrCodeObjectLockConfigurationNotFound) { + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error deleting S3 bucket Object Lock configuration (%s): %w", d.Id(), err)) + } + + return nil +} + +func resourceBucketObjectLockConfigurationCreateResourceID(bucket, expectedBucketOwner string) string { + if bucket == "" { + return expectedBucketOwner + } + + if expectedBucketOwner == "" { + return bucket + } + + parts := []string{bucket, expectedBucketOwner} + id := strings.Join(parts, ",") + + return id +} + +func resourceBucketObjectLockConfigurationParseResourceID(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) +} + +func expandBucketObjectLockConfigurationRule(l []interface{}) *s3.ObjectLockRule { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + if !ok { + return nil + } + + rule := &s3.ObjectLockRule{} + + if v, ok := tfMap["default_retention"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + rule.DefaultRetention = expandBucketObjectLockConfigurationCorsRuleDefaultRetention(v) + } + + return rule +} + +func expandBucketObjectLockConfigurationCorsRuleDefaultRetention(l []interface{}) *s3.DefaultRetention { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + if !ok { + return nil + } + + dr := &s3.DefaultRetention{} + + if v, ok := tfMap["days"].(int); ok && v > 0 { + dr.Days = aws.Int64(int64(v)) + } + + if v, ok := tfMap["mode"].(string); ok && v != "" { + dr.Mode = aws.String(v) + } + + if v, ok := tfMap["years"].(int); ok && v > 0 { + dr.Years = aws.Int64(int64(v)) + } + + return dr +} + +func flattenBucketObjectLockConfigurationRule(rule *s3.ObjectLockRule) []interface{} { + if rule == nil { + return []interface{}{} + } + + m := make(map[string]interface{}) + + if rule.DefaultRetention != nil { + m["default_retention"] = flattenBucketObjectLockConfigurationRuleDefaultRetention(rule.DefaultRetention) + } + return []interface{}{m} +} + +func flattenBucketObjectLockConfigurationRuleDefaultRetention(dr *s3.DefaultRetention) []interface{} { + if dr == nil { + return []interface{}{} + } + + m := make(map[string]interface{}) + + if dr.Days != nil { + m["days"] = int(aws.Int64Value(dr.Days)) + } + + if dr.Mode != nil { + m["mode"] = aws.StringValue(dr.Mode) + } + + if dr.Years != nil { + m["years"] = int(aws.Int64Value(dr.Years)) + } + + return []interface{}{m} +} diff --git a/internal/service/s3/bucket_object_lock_configuration_test.go b/internal/service/s3/bucket_object_lock_configuration_test.go new file mode 100644 index 00000000000..a00a72598b0 --- /dev/null +++ b/internal/service/s3/bucket_object_lock_configuration_test.go @@ -0,0 +1,222 @@ +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" + tfs3 "github.com/hashicorp/terraform-provider-aws/internal/service/s3" +) + +func TestAccS3BucketObjectLockConfiguration_basic(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_object_lock_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketObjectLockConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketObjectLockConfigurationBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketObjectLockConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "object_lock_enabled", s3.ObjectLockEnabledEnabled), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rule.0.default_retention.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rule.0.default_retention.0.days", "3"), + resource.TestCheckResourceAttr(resourceName, "rule.0.default_retention.0.mode", s3.ObjectLockRetentionModeCompliance), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3BucketObjectLockConfiguration_disappears(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_object_lock_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketObjectLockConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketObjectLockConfigurationBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketObjectLockConfigurationExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfs3.ResourceBucketObjectLockConfiguration(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccS3BucketObjectLockConfiguration_update(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_object_lock_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketObjectLockConfigurationBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketObjectLockConfigurationExists(resourceName), + ), + }, + { + Config: testAccBucketObjectLockConfigurationUpdateConfig(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "object_lock_enabled", s3.ObjectLockEnabledEnabled), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rule.0.default_retention.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rule.0.default_retention.0.years", "1"), + resource.TestCheckResourceAttr(resourceName, "rule.0.default_retention.0.mode", s3.ObjectLockRetentionModeGovernance), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckBucketObjectLockConfigurationDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).S3Conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_s3_bucket_object_lock_configuration" { + continue + } + + input := &s3.GetObjectLockConfigurationInput{ + Bucket: aws.String(rs.Primary.ID), + } + + output, err := conn.GetObjectLockConfiguration(input) + + if tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket, tfs3.ErrCodeObjectLockConfigurationNotFound) { + continue + } + + if err != nil { + return fmt.Errorf("error getting S3 Bucket Object Lock configuration (%s): %w", rs.Primary.ID, err) + } + + if output != nil { + return fmt.Errorf("S3 Bucket Object Lock configuration (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckBucketObjectLockConfigurationExists(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.GetObjectLockConfigurationInput{ + Bucket: aws.String(rs.Primary.ID), + } + + output, err := conn.GetObjectLockConfiguration(input) + + if err != nil { + return fmt.Errorf("error getting S3 Bucket Object Lock configuration (%s): %w", rs.Primary.ID, err) + } + + if output == nil || output.ObjectLockConfiguration == nil { + return fmt.Errorf("S3 Bucket Object Lock configuration (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccBucketObjectLockConfigurationBasicConfig(bucketName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + + object_lock_configuration { + object_lock_enabled = "Enabled" + } + + lifecycle { + ignore_changes = [ + object_lock_configuration + ] + } +} + +resource "aws_s3_bucket_object_lock_configuration" "test" { + bucket = aws_s3_bucket.test.id + + rule { + default_retention { + mode = %[2]q + days = 3 + } + } +} +`, bucketName, s3.ObjectLockRetentionModeCompliance) +} + +func testAccBucketObjectLockConfigurationUpdateConfig(bucketName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + + object_lock_configuration { + object_lock_enabled = "Enabled" + } + + lifecycle { + ignore_changes = [ + object_lock_configuration + ] + } +} + +resource "aws_s3_bucket_object_lock_configuration" "test" { + bucket = aws_s3_bucket.test.id + + rule { + default_retention { + mode = %[2]q + years = 1 + } + } +} +`, bucketName, s3.ObjectLockModeGovernance) +} diff --git a/internal/service/s3/errors.go b/internal/service/s3/errors.go index caef77777a4..bd78fbf5ee9 100644 --- a/internal/service/s3/errors.go +++ b/internal/service/s3/errors.go @@ -8,5 +8,6 @@ const ( ErrCodeNoSuchCORSConfiguration = "NoSuchCORSConfiguration" ErrCodeNoSuchPublicAccessBlockConfiguration = "NoSuchPublicAccessBlockConfiguration" ErrCodeNoSuchWebsiteConfiguration = "NoSuchWebsiteConfiguration" + ErrCodeObjectLockConfigurationNotFound = "ObjectLockConfigurationNotFoundError" ErrCodeOperationAborted = "OperationAborted" ) diff --git a/website/docs/r/s3_bucket_object_lock_configuration.html.markdown b/website/docs/r/s3_bucket_object_lock_configuration.html.markdown new file mode 100644 index 00000000000..325308fd61a --- /dev/null +++ b/website/docs/r/s3_bucket_object_lock_configuration.html.markdown @@ -0,0 +1,79 @@ +--- +subcategory: "S3" +layout: "aws" +page_title: "AWS: aws_s3_bucket_object_lock_configuration" +description: |- + Provides an S3 bucket Object Lock configuration resource. +--- + +# Resource: aws_s3_bucket_object_lock_configuration + +Provides an S3 bucket Object Lock configuration resource. For more information about Object Locking, go to [Using S3 Object Lock](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html) in the Amazon S3 User Guide. + +~> **NOTE:** You can only enable Object Lock for new buckets. If you want to turn on Object Lock for an existing bucket, contact AWS Support. + +## Example Usage + +```terraform +resource "aws_s3_bucket" "example" { + bucket = "mybucket" + + object_lock_configuration { + object_lock_enabled = "Enabled" + } +} + +resource "aws_s3_bucket_object_lock_configuration" "example" { + bucket = aws_s3_bucket.example.bucket + + rule { + default_retention { + mode = "COMPLIANCE" + days = 5 + } + } +} +``` + +## 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. +* `object_lock_enabled` - (Optional, Forces new resource) Indicates whether this bucket has an Object Lock configuration enabled. Defaults to `Enabled`. Valid values: `Enabled`. +* `rule` - (Required) Configuration block for specifying the Object Lock rule for the specified object [detailed below](#rule). + +### rule + +The `rule` configuration block supports the following arguments: + +* `default_retention` - (Required) A configuration block for specifying the default Object Lock retention settings for new objects placed in the specified bucket [detailed below](#default_retention). + +### default_retention + +The `default_retention` configuration block supports the following arguments: + +* `days` - (Optional, Required if `years` is not specified) The number of days that you want to specify for the default retention period. +* `mode` - (Required) The default Object Lock retention mode you want to apply to new objects placed in the specified bucket. Valid values: `COMPLIANCE`, `GOVERNANCE`. +* `years` - (Optional, Required if `days` is not specified) The number of years that you want to specify for the default retention period. + +## 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 Object Lock configuration can be imported using the `bucket` e.g., + +``` +$ terraform import aws_s3_bucket_object_lock_configuration.example bucket-name +``` + +In addition, S3 bucket Object Lock configuration can be imported using the `bucket` and `expected_bucket_owner` separated by a comma (`,`) e.g., + +``` +$ terraform import aws_s3_bucket_object_lock_configuration.example bucket-name,123456789012 +``` From 3ec6d2a563ce2689a5be58f6d5706280ff7bb538 Mon Sep 17 00:00:00 2001 From: Angie Pinilla Date: Tue, 18 Jan 2022 21:32:49 -0500 Subject: [PATCH 2/5] Update CHANGELOG for #22644 --- .changelog/22644.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/22644.txt diff --git a/.changelog/22644.txt b/.changelog/22644.txt new file mode 100644 index 00000000000..3cf0545a975 --- /dev/null +++ b/.changelog/22644.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_s3_bucket_object_lock_configuration +``` \ No newline at end of file From ba102ae5fbcbfc8364300851bfcf49eb9a844b78 Mon Sep 17 00:00:00 2001 From: Angie Pinilla Date: Wed, 2 Feb 2022 20:57:03 -0500 Subject: [PATCH 3/5] update imports referencing tfawserr --- internal/service/s3/bucket_object_lock_configuration.go | 2 +- internal/service/s3/bucket_object_lock_configuration_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/s3/bucket_object_lock_configuration.go b/internal/service/s3/bucket_object_lock_configuration.go index 0e8b8ac835b..890492cfb87 100644 --- a/internal/service/s3/bucket_object_lock_configuration.go +++ b/internal/service/s3/bucket_object_lock_configuration.go @@ -8,7 +8,7 @@ import ( "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/aws-sdk-go-base/v2/awsv1shim/v2/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" diff --git a/internal/service/s3/bucket_object_lock_configuration_test.go b/internal/service/s3/bucket_object_lock_configuration_test.go index a00a72598b0..b89935044b2 100644 --- a/internal/service/s3/bucket_object_lock_configuration_test.go +++ b/internal/service/s3/bucket_object_lock_configuration_test.go @@ -6,7 +6,7 @@ import ( "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/aws-sdk-go-base/v2/awsv1shim/v2/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" From 53c0e6016ba31a937ab84d2b335ad2409af7881a Mon Sep 17 00:00:00 2001 From: Angie Pinilla Date: Thu, 3 Feb 2022 01:23:58 -0500 Subject: [PATCH 4/5] r/s3_bucket_object_lock_configuration: re-use share id create/parse method --- .../s3/bucket_object_lock_configuration.go | 38 ++----------------- .../bucket_object_lock_configuration_test.go | 22 ++++++++++- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/internal/service/s3/bucket_object_lock_configuration.go b/internal/service/s3/bucket_object_lock_configuration.go index 890492cfb87..7534bee398c 100644 --- a/internal/service/s3/bucket_object_lock_configuration.go +++ b/internal/service/s3/bucket_object_lock_configuration.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/s3" @@ -124,7 +123,7 @@ func resourceBucketObjectLockConfigurationCreate(ctx context.Context, d *schema. return diag.FromErr(fmt.Errorf("error creating S3 bucket (%s) Object Lock configuration: %w", bucket, err)) } - d.SetId(resourceBucketObjectLockConfigurationCreateResourceID(bucket, expectedBucketOwner)) + d.SetId(CreateResourceID(bucket, expectedBucketOwner)) return resourceBucketObjectLockConfigurationRead(ctx, d, meta) } @@ -132,7 +131,7 @@ func resourceBucketObjectLockConfigurationCreate(ctx context.Context, d *schema. func resourceBucketObjectLockConfigurationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).S3Conn - bucket, expectedBucketOwner, err := resourceBucketObjectLockConfigurationParseResourceID(d.Id()) + bucket, expectedBucketOwner, err := ParseResourceID(d.Id()) if err != nil { return diag.FromErr(err) } @@ -182,7 +181,7 @@ func resourceBucketObjectLockConfigurationRead(ctx context.Context, d *schema.Re func resourceBucketObjectLockConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).S3Conn - bucket, expectedBucketOwner, err := resourceBucketObjectLockConfigurationParseResourceID(d.Id()) + bucket, expectedBucketOwner, err := ParseResourceID(d.Id()) if err != nil { return diag.FromErr(err) } @@ -221,7 +220,7 @@ func resourceBucketObjectLockConfigurationUpdate(ctx context.Context, d *schema. func resourceBucketObjectLockConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).S3Conn - bucket, expectedBucketOwner, err := resourceBucketObjectLockConfigurationParseResourceID(d.Id()) + bucket, expectedBucketOwner, err := ParseResourceID(d.Id()) if err != nil { return diag.FromErr(err) } @@ -252,35 +251,6 @@ func resourceBucketObjectLockConfigurationDelete(ctx context.Context, d *schema. return nil } -func resourceBucketObjectLockConfigurationCreateResourceID(bucket, expectedBucketOwner string) string { - if bucket == "" { - return expectedBucketOwner - } - - if expectedBucketOwner == "" { - return bucket - } - - parts := []string{bucket, expectedBucketOwner} - id := strings.Join(parts, ",") - - return id -} - -func resourceBucketObjectLockConfigurationParseResourceID(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) -} - func expandBucketObjectLockConfigurationRule(l []interface{}) *s3.ObjectLockRule { if len(l) == 0 || l[0] == nil { return nil diff --git a/internal/service/s3/bucket_object_lock_configuration_test.go b/internal/service/s3/bucket_object_lock_configuration_test.go index b89935044b2..b7f470e210c 100644 --- a/internal/service/s3/bucket_object_lock_configuration_test.go +++ b/internal/service/s3/bucket_object_lock_configuration_test.go @@ -110,8 +110,17 @@ func testAccCheckBucketObjectLockConfigurationDestroy(s *terraform.State) error continue } + bucket, expectedBucketOwner, err := tfs3.ParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + input := &s3.GetObjectLockConfigurationInput{ - Bucket: aws.String(rs.Primary.ID), + Bucket: aws.String(bucket), + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) } output, err := conn.GetObjectLockConfiguration(input) @@ -145,8 +154,17 @@ func testAccCheckBucketObjectLockConfigurationExists(resourceName string) resour conn := acctest.Provider.Meta().(*conns.AWSClient).S3Conn + bucket, expectedBucketOwner, err := tfs3.ParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + input := &s3.GetObjectLockConfigurationInput{ - Bucket: aws.String(rs.Primary.ID), + Bucket: aws.String(bucket), + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) } output, err := conn.GetObjectLockConfiguration(input) From 0a8c9d5becf1e164189b2618a2f5f1eabe507319 Mon Sep 17 00:00:00 2001 From: angie pinilla Date: Thu, 3 Feb 2022 09:28:19 -0500 Subject: [PATCH 5/5] Update website/docs/r/s3_bucket_object_lock_configuration.html.markdown --- website/docs/r/s3_bucket_object_lock_configuration.html.markdown | 1 + 1 file changed, 1 insertion(+) diff --git a/website/docs/r/s3_bucket_object_lock_configuration.html.markdown b/website/docs/r/s3_bucket_object_lock_configuration.html.markdown index 325308fd61a..4bf551b0448 100644 --- a/website/docs/r/s3_bucket_object_lock_configuration.html.markdown +++ b/website/docs/r/s3_bucket_object_lock_configuration.html.markdown @@ -43,6 +43,7 @@ The following arguments are supported: * `expected_bucket_owner` - (Optional, Forces new resource) The account ID of the expected bucket owner. * `object_lock_enabled` - (Optional, Forces new resource) Indicates whether this bucket has an Object Lock configuration enabled. Defaults to `Enabled`. Valid values: `Enabled`. * `rule` - (Required) Configuration block for specifying the Object Lock rule for the specified object [detailed below](#rule). +* `token` - (Optional) A token to allow Object Lock to be enabled for an existing bucket. ### rule