Skip to content

Commit

Permalink
Merge pull request #6964 from ewbankkit/issue-6872
Browse files Browse the repository at this point in the history
r/aws_s3_bucket: Support S3 Object Lock
  • Loading branch information
bflad authored Jan 16, 2019
2 parents 46c722b + bc6ace7 commit 6c21807
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 0 deletions.
180 changes: 180 additions & 0 deletions aws/resource_aws_s3_bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,64 @@ func resourceAwsS3Bucket() *schema.Resource {
},
},

"object_lock_configuration": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"object_lock_enabled": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{
s3.ObjectLockEnabledEnabled,
}, false),
},

"rule": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"default_retention": {
Type: schema.TypeList,
Required: true,
MinItems: 1,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"mode": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
s3.ObjectLockModeGovernance,
s3.ObjectLockModeCompliance,
}, false),
},

"days": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntAtLeast(1),
},

"years": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntAtLeast(1),
},
},
},
},
},
},
},
},
},
},

"tags": tagsSchema(),
},
}
Expand Down Expand Up @@ -577,6 +635,12 @@ func resourceAwsS3BucketCreate(d *schema.ResourceData, meta interface{}) error {
return fmt.Errorf("Error validating S3 bucket name: %s", err)
}

// S3 Object Lock can only be enabled on bucket creation.
objectLockConfiguration := expandS3ObjectLockConfiguration(d.Get("object_lock_configuration").([]interface{}))
if objectLockConfiguration != nil && aws.StringValue(objectLockConfiguration.ObjectLockEnabled) == s3.ObjectLockEnabledEnabled {
req.ObjectLockEnabledForBucket = aws.Bool(true)
}

err := resource.Retry(5*time.Minute, func() *resource.RetryError {
log.Printf("[DEBUG] Trying to create new S3 bucket: %q", bucket)
_, err := s3conn.CreateBucket(req)
Expand Down Expand Up @@ -675,6 +739,12 @@ func resourceAwsS3BucketUpdate(d *schema.ResourceData, meta interface{}) error {
}
}

if d.HasChange("object_lock_configuration") {
if err := resourceAwsS3BucketObjectLockConfigurationUpdate(s3conn, d); err != nil {
return err
}
}

return resourceAwsS3BucketRead(d, meta)
}

Expand Down Expand Up @@ -1082,6 +1152,15 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
return fmt.Errorf("error setting server_side_encryption_configuration: %s", err)
}

// Object Lock configuration.
if conf, err := readS3ObjectLockConfiguration(s3conn, d.Id()); err != nil {
return fmt.Errorf("error getting S3 Bucket Object Lock configuration: %s", err)
} else {
if err := d.Set("object_lock_configuration", conf); err != nil {
return fmt.Errorf("error setting object_lock_configuration: %s", err)
}
}

// Add the region as an attribute

locationResponse, err := retryOnAwsCode("NoSuchBucket", func() (interface{}, error) {
Expand Down Expand Up @@ -1729,6 +1808,23 @@ func resourceAwsS3BucketServerSideEncryptionConfigurationUpdate(s3conn *s3.S3, d
return nil
}

func resourceAwsS3BucketObjectLockConfigurationUpdate(s3conn *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)),
ObjectLockConfiguration: expandS3ObjectLockConfiguration(d.Get("object_lock_configuration").([]interface{})),
}

_, err := retryOnAwsCode(s3.ErrCodeNoSuchBucket, func() (interface{}, error) {
return s3conn.PutObjectLockConfiguration(req)
})
if err != nil {
return fmt.Errorf("error putting S3 object lock configuration: %s", err)
}

return nil
}

func resourceAwsS3BucketReplicationConfigurationUpdate(s3conn *s3.S3, d *schema.ResourceData) error {
bucket := d.Get("bucket").(string)
replicationConfiguration := d.Get("replication_configuration").([]interface{})
Expand Down Expand Up @@ -2370,3 +2466,87 @@ func sourceSseKmsObjectsHash(v interface{}) int {
type S3Website struct {
Endpoint, Domain string
}

//
// S3 Object Lock functions.
//

func readS3ObjectLockConfiguration(conn *s3.S3, bucket string) (interface{}, error) {
resp, err := retryOnAwsCode(s3.ErrCodeNoSuchBucket, func() (interface{}, error) {
return conn.GetObjectLockConfiguration(&s3.GetObjectLockConfigurationInput{
Bucket: aws.String(bucket),
})
})
if err != nil {
if isAWSErr(err, "ObjectLockConfigurationNotFoundError", "") {
return nil, nil
}
return nil, err
}

return flattenS3ObjectLockConfiguration(resp.(*s3.GetObjectLockConfigurationOutput).ObjectLockConfiguration), nil
}

func expandS3ObjectLockConfiguration(vConf []interface{}) *s3.ObjectLockConfiguration {
if len(vConf) == 0 || vConf[0] == nil {
return nil
}

mConf := vConf[0].(map[string]interface{})

conf := &s3.ObjectLockConfiguration{}

if vObjectLockEnabled, ok := mConf["object_lock_enabled"].(string); ok && vObjectLockEnabled != "" {
conf.ObjectLockEnabled = aws.String(vObjectLockEnabled)
}

if vRule, ok := mConf["rule"].([]interface{}); ok && len(vRule) > 0 {
mRule := vRule[0].(map[string]interface{})

if vDefaultRetention, ok := mRule["default_retention"].([]interface{}); ok && len(vDefaultRetention) > 0 && vDefaultRetention[0] != nil {
mDefaultRetention := vDefaultRetention[0].(map[string]interface{})

conf.Rule = &s3.ObjectLockRule{
DefaultRetention: &s3.DefaultRetention{},
}

if vMode, ok := mDefaultRetention["mode"].(string); ok && vMode != "" {
conf.Rule.DefaultRetention.Mode = aws.String(vMode)
}
if vDays, ok := mDefaultRetention["days"].(int); ok && vDays > 0 {
conf.Rule.DefaultRetention.Days = aws.Int64(int64(vDays))
}
if vYears, ok := mDefaultRetention["years"].(int); ok && vYears > 0 {
conf.Rule.DefaultRetention.Years = aws.Int64(int64(vYears))
}
}
}

return conf
}

func flattenS3ObjectLockConfiguration(conf *s3.ObjectLockConfiguration) []interface{} {
if conf == nil {
return []interface{}{}
}

mConf := map[string]interface{}{
"object_lock_enabled": aws.StringValue(conf.ObjectLockEnabled),
}

if conf.Rule != nil && conf.Rule.DefaultRetention != nil {
mRule := map[string]interface{}{
"default_retention": []interface{}{
map[string]interface{}{
"mode": aws.StringValue(conf.Rule.DefaultRetention.Mode),
"days": int(aws.Int64Value(conf.Rule.DefaultRetention.Days)),
"years": int(aws.Int64Value(conf.Rule.DefaultRetention.Years)),
},
},
}

mConf["rule"] = []interface{}{mRule}
}

return []interface{}{mConf}
}
63 changes: 63 additions & 0 deletions aws/resource_aws_s3_bucket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,38 @@ func TestAccAWSS3Bucket_ReplicationSchemaV2(t *testing.T) {
})
}

func TestAccAWSS3Bucket_objectLock(t *testing.T) {
rInt := acctest.RandInt()

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSS3BucketDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSS3BucketObjectLockEnabledNoDefaultRetention(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSS3BucketExists("aws_s3_bucket.arbitrary"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "object_lock_configuration.#", "1"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "object_lock_configuration.0.object_lock_enabled", "Enabled"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "object_lock_configuration.0.rule.#", "0"),
),
},
{
Config: testAccAWSS3BucketObjectLockEnabledWithDefaultRetention(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSS3BucketExists("aws_s3_bucket.arbitrary"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "object_lock_configuration.#", "1"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "object_lock_configuration.0.object_lock_enabled", "Enabled"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "object_lock_configuration.0.rule.#", "1"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "object_lock_configuration.0.rule.0.default_retention.0.mode", "COMPLIANCE"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "object_lock_configuration.0.rule.0.default_retention.0.days", "3"),
),
},
},
})
}

func TestAWSS3BucketName(t *testing.T) {
validDnsNames := []string{
"foobar",
Expand Down Expand Up @@ -2953,6 +2985,37 @@ resource "aws_s3_bucket" "destination" {
`, randInt, randInt, randInt)
}

func testAccAWSS3BucketObjectLockEnabledNoDefaultRetention(randInt int) string {
return fmt.Sprintf(`
resource "aws_s3_bucket" "arbitrary" {
bucket = "tf-test-bucket-%d"
object_lock_configuration {
object_lock_enabled = "Enabled"
}
}
`, randInt)
}

func testAccAWSS3BucketObjectLockEnabledWithDefaultRetention(randInt int) string {
return fmt.Sprintf(`
resource "aws_s3_bucket" "arbitrary" {
bucket = "tf-test-bucket-%d"
object_lock_configuration {
object_lock_enabled = "Enabled"
rule {
default_retention {
mode = "COMPLIANCE"
days = 3
}
}
}
}
`, randInt)
}

const testAccAWSS3BucketConfig_namePrefix = `
resource "aws_s3_bucket" "test" {
bucket_prefix = "tf-test-"
Expand Down
22 changes: 22 additions & 0 deletions website/docs/r/s3_bucket.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ the costs of any data transfer. See [Requester Pays Buckets](http://docs.aws.ama
developer guide for more information.
* `replication_configuration` - (Optional) A configuration of [replication configuration](http://docs.aws.amazon.com/AmazonS3/latest/dev/crr.html) (documented below).
* `server_side_encryption_configuration` - (Optional) A configuration of [server-side encryption configuration](http://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-encryption.html) (documented below)
* `object_lock_configuration` - (Optional) A configuration of [S3 object locking](https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock.html) (documented below)

~> **NOTE:** You cannot use `acceleration_status` in `cn-north-1` or `us-gov-west-1`

Expand Down Expand Up @@ -461,6 +462,27 @@ The `access_control_translation` object supports the following:

* `owner` - (Required) The override value for the owner on replicated objects. Currently only `Destination` is supported.

The `object_lock_configuration` object supports the following:

* `object_lock_enabled` - (Required) Indicates whether this bucket has an Object Lock configuration enabled. Valid value is `Enabled`.
* `rule` - (Optional) The Object Lock rule in place for this bucket.

The `rule` object supports the following:

* `default_retention` - (Required) The default retention period that you want to apply to new objects placed in this bucket.

The `default_retention` object supports the following:

* `mode` - (Required) The default Object Lock retention mode you want to apply to new objects placed in this bucket. Valid values are `GOVERNANCE` and `COMPLIANCE`.
* `days` - (Optional) The number of days that you want to specify for the default retention period.
* `years` - (Optional) The number of years that you want to specify for the default retention period.

Either `days` or `years` must be specified, but not both.

~> **NOTE on `object_lock_configuration`:** You can only enable S3 Object Lock for new buckets. If you need to turn on S3 Object Lock for an existing bucket, please contact AWS Support.
When you create a bucket with S3 Object Lock enabled, Amazon S3 automatically enables versioning for the bucket.
Once you create a bucket with S3 Object Lock enabled, you can't disable Object Lock or suspend versioning for the bucket.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:
Expand Down

0 comments on commit 6c21807

Please sign in to comment.