From f5b46b80e7f5c7f4397b909609350e3ae9b9e705 Mon Sep 17 00:00:00 2001 From: Krzysztof Wilczynski Date: Wed, 10 Aug 2016 13:05:39 +0900 Subject: [PATCH] Add ability to set canned ACL in aws_s3_bucket_object. (#8091) An S3 Bucket owner may wish to set a canned ACL (as opposite to explicitly set grantees, etc.) for an object. This commit adds an optional "acl" attribute to the aws_s3_bucket_object resource so that the owner of the S3 bucket can specify an appropriate pre-defined ACL to use when creating an object. Signed-off-by: Krzysztof Wilczynski --- .../aws/resource_aws_s3_bucket_object.go | 46 +++++++ .../aws/resource_aws_s3_bucket_object_test.go | 114 ++++++++++++++++++ .../aws/r/s3_bucket_object.html.markdown | 5 +- 3 files changed, 163 insertions(+), 2 deletions(-) diff --git a/builtin/providers/aws/resource_aws_s3_bucket_object.go b/builtin/providers/aws/resource_aws_s3_bucket_object.go index c7ae47d75646..2df9d5da02df 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket_object.go +++ b/builtin/providers/aws/resource_aws_s3_bucket_object.go @@ -6,6 +6,7 @@ import ( "io" "log" "os" + "sort" "strings" "github.com/hashicorp/terraform/helper/schema" @@ -30,6 +31,13 @@ func resourceAwsS3BucketObject() *schema.Resource { ForceNew: true, }, + "acl": &schema.Schema{ + Type: schema.TypeString, + Default: "private", + Optional: true, + ValidateFunc: validateS3BucketObjectAclType, + }, + "cache_control": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -101,6 +109,7 @@ func resourceAwsS3BucketObjectPut(d *schema.ResourceData, meta interface{}) erro bucket := d.Get("bucket").(string) key := d.Get("key").(string) + acl := d.Get("acl").(string) var body io.ReadSeeker if v, ok := d.GetOk("source"); ok { @@ -131,6 +140,7 @@ func resourceAwsS3BucketObjectPut(d *schema.ResourceData, meta interface{}) erro putInput := &s3.PutObjectInput{ Bucket: aws.String(bucket), Key: aws.String(key), + ACL: aws.String(acl), Body: body, } @@ -251,3 +261,39 @@ func resourceAwsS3BucketObjectDelete(d *schema.ResourceData, meta interface{}) e return nil } + +func validateS3BucketObjectAclType(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + cannedAcls := map[string]bool{ + s3.ObjectCannedACLPrivate: true, + s3.ObjectCannedACLPublicRead: true, + s3.ObjectCannedACLPublicReadWrite: true, + s3.ObjectCannedACLAuthenticatedRead: true, + s3.ObjectCannedACLAwsExecRead: true, + s3.ObjectCannedACLBucketOwnerRead: true, + s3.ObjectCannedACLBucketOwnerFullControl: true, + } + + sentenceJoin := func(m map[string]bool) string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, fmt.Sprintf("%q", k)) + } + sort.Strings(keys) + + length := len(keys) + words := make([]string, length) + copy(words, keys) + + words[length-1] = fmt.Sprintf("or %s", words[length-1]) + return strings.Join(words, ", ") + } + + if _, ok := cannedAcls[value]; !ok { + errors = append(errors, fmt.Errorf( + "%q contains an invalid canned ACL type %q. Valid types are either %s", + k, value, sentenceJoin(cannedAcls))) + } + return +} diff --git a/builtin/providers/aws/resource_aws_s3_bucket_object_test.go b/builtin/providers/aws/resource_aws_s3_bucket_object_test.go index 63ccf68618b1..d88b3c99dc75 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket_object_test.go +++ b/builtin/providers/aws/resource_aws_s3_bucket_object_test.go @@ -4,6 +4,8 @@ import ( "fmt" "io/ioutil" "os" + "reflect" + "sort" "testing" "github.com/hashicorp/terraform/helper/acctest" @@ -265,6 +267,104 @@ func TestAccAWSS3BucketObject_kms(t *testing.T) { }) } +func TestAccAWSS3BucketObject_acl(t *testing.T) { + rInt := acctest.RandInt() + var obj s3.GetObjectOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketObjectDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSS3BucketObjectConfig_acl(rInt, "private"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketObjectExists( + "aws_s3_bucket_object.object", &obj), + resource.TestCheckResourceAttr( + "aws_s3_bucket_object.object", + "acl", + "private"), + testAccCheckAWSS3BucketObjectAcl( + "aws_s3_bucket_object.object", + []string{"FULL_CONTROL"}), + ), + }, + resource.TestStep{ + Config: testAccAWSS3BucketObjectConfig_acl(rInt, "public-read"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketObjectExists( + "aws_s3_bucket_object.object", + &obj), + resource.TestCheckResourceAttr( + "aws_s3_bucket_object.object", + "acl", + "public-read"), + testAccCheckAWSS3BucketObjectAcl( + "aws_s3_bucket_object.object", + []string{"FULL_CONTROL", "READ"}), + ), + }, + }, + }) +} + +func TestResourceAWSS3BucketObjectAcl_validation(t *testing.T) { + _, errors := validateS3BucketObjectAclType("incorrect", "acl") + if len(errors) == 0 { + t.Fatalf("Expected to trigger a validation error") + } + + var testCases = []struct { + Value string + ErrCount int + }{ + { + Value: "public-read", + ErrCount: 0, + }, + { + Value: "public-read-write", + ErrCount: 0, + }, + } + + for _, tc := range testCases { + _, errors := validateS3BucketObjectAclType(tc.Value, "acl") + if len(errors) != tc.ErrCount { + t.Fatalf("Expected not to trigger a validation error") + } + } +} + +func testAccCheckAWSS3BucketObjectAcl(n string, expectedPerms []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, _ := s.RootModule().Resources[n] + s3conn := testAccProvider.Meta().(*AWSClient).s3conn + + out, err := s3conn.GetObjectAcl(&s3.GetObjectAclInput{ + Bucket: aws.String(rs.Primary.Attributes["bucket"]), + Key: aws.String(rs.Primary.Attributes["key"]), + }) + + if err != nil { + return fmt.Errorf("GetObjectAcl error: %v", err) + } + + var perms []string + for _, v := range out.Grants { + perms = append(perms, *v.Permission) + } + sort.Strings(perms) + + if !reflect.DeepEqual(perms, expectedPerms) { + return fmt.Errorf("Expected ACL permissions to be %v, got %v", expectedPerms, perms) + } + + return nil + } +} + func testAccAWSS3BucketObjectConfigSource(randInt int, source string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "object_bucket" { @@ -358,3 +458,17 @@ resource "aws_s3_bucket_object" "object" { } `, randInt) } + +func testAccAWSS3BucketObjectConfig_acl(randInt int, acl string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "object_bucket" { + bucket = "tf-object-test-bucket-%d" +} +resource "aws_s3_bucket_object" "object" { + bucket = "${aws_s3_bucket.object_bucket.bucket}" + key = "test-key" + content = "some_bucket_content" + acl = "%s" +} +`, randInt, acl) +} diff --git a/website/source/docs/providers/aws/r/s3_bucket_object.html.markdown b/website/source/docs/providers/aws/r/s3_bucket_object.html.markdown index c34997c0840d..fc7f95b53431 100644 --- a/website/source/docs/providers/aws/r/s3_bucket_object.html.markdown +++ b/website/source/docs/providers/aws/r/s3_bucket_object.html.markdown @@ -52,14 +52,15 @@ The following arguments are supported: * `key` - (Required) The name of the object once it is in the bucket. * `source` - (Required) The path to the source file being uploaded to the bucket. * `content` - (Required unless `source` given) The literal content being uploaded to the bucket. +* `acl` - (Optional) The [canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) to apply. Defaults to "private". * `cache_control` - (Optional) Specifies caching behavior along the request/reply chain Read [w3c cache_control](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9) for further details. * `content_disposition` - (Optional) Specifies presentational information for the object. Read [wc3 content_disposition](http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.5.1) for further information. * `content_encoding` - (Optional) Specifies what content encodings have been applied to the object and thus what decoding mechanisms must be applied to obtain the media-type referenced by the Content-Type header field. Read [w3c content encoding](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11) for further information. * `content_language` - (Optional) The language the content is in e.g. en-US or en-GB. * `content_type` - (Optional) A standard MIME type describing the format of the object data, e.g. application/octet-stream. All Valid MIME Types are valid for this input. -* `etag` - (Optional) Used to trigger updates. The only meaningful value is `${md5(file("path/to/file"))}`. +* `etag` - (Optional) Used to trigger updates. The only meaningful value is `${md5(file("path/to/file"))}`. This attribute is not compatible with `kms_key_id` -* `kms_key_id` - (Optional) Specifies the AWS KMS Key ID to use for object encryption. +* `kms_key_id` - (Optional) Specifies the AWS KMS Key ID to use for object encryption. This value is a fully qualified **ARN** of the KMS Key. If using `aws_kms_key`, use the exported `arn` attribute: `kms_key_id = "${aws_kms_key.foo.arn}"`