Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

r/aws_s3_bucket: Support S3 Object Lock #6964

Merged
merged 2 commits into from
Jan 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
ewbankkit marked this conversation as resolved.
Show resolved Hide resolved

~> **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