From e0632de30c3582520444699da181989d88c5907b Mon Sep 17 00:00:00 2001 From: Kazunori Kojima Date: Fri, 2 Oct 2015 01:49:32 +0900 Subject: [PATCH 1/2] Add support S3 CORS --- .../providers/aws/resource_aws_s3_bucket.go | 117 ++++++++++++++++++ .../aws/resource_aws_s3_bucket_test.go | 62 ++++++++++ .../providers/aws/r/s3_bucket.html.markdown | 26 ++++ 3 files changed, 205 insertions(+) diff --git a/builtin/providers/aws/resource_aws_s3_bucket.go b/builtin/providers/aws/resource_aws_s3_bucket.go index a329d4ff6..93105ec51 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket.go +++ b/builtin/providers/aws/resource_aws_s3_bucket.go @@ -41,6 +41,39 @@ func resourceAwsS3Bucket() *schema.Resource { StateFunc: normalizeJson, }, + "cors_rule": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allowed_headers": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "allowed_methods": &schema.Schema{ + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "allowed_origins": &schema.Schema{ + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "expose_headers": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "max_age_seconds": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + "website": &schema.Schema{ Type: schema.TypeList, Optional: true, @@ -168,6 +201,12 @@ func resourceAwsS3BucketUpdate(d *schema.ResourceData, meta interface{}) error { } } + if d.HasChange("cors_rule") { + if err := resourceAwsS3BucketCorsUpdate(s3conn, d); err != nil { + return err + } + } + if d.HasChange("website") { if err := resourceAwsS3BucketWebsiteUpdate(s3conn, d); err != nil { return err @@ -221,6 +260,25 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error { } } + // Read the CORS + cors, err := s3conn.GetBucketCors(&s3.GetBucketCorsInput{ + Bucket: aws.String(d.Id()), + }) + log.Printf("[DEBUG] S3 bucket: %s, read CORS: %v", d.Id(), cors) + if err != nil { + rules := make([]map[string]interface{}, 0, len(cors.CORSRules)) + for _, ruleObject := range cors.CORSRules { + rule := make(map[string]interface{}) + rule["allowed_headers"] = ruleObject.AllowedHeaders + rule["allowed_methods"] = ruleObject.AllowedMethods + rule["allowed_origins"] = ruleObject.AllowedOrigins + rule["expose_headers"] = ruleObject.ExposeHeaders + rule["max_age_seconds"] = ruleObject.MaxAgeSeconds + rules = append(rules, rule) + } + d.Set("cors_rule", rules) + } + // Read the website configuration ws, err := s3conn.GetBucketWebsite(&s3.GetBucketWebsiteInput{ Bucket: aws.String(d.Id()), @@ -400,6 +458,65 @@ func resourceAwsS3BucketPolicyUpdate(s3conn *s3.S3, d *schema.ResourceData) erro return nil } +func resourceAwsS3BucketCorsUpdate(s3conn *s3.S3, d *schema.ResourceData) error { + bucket := d.Get("bucket").(string) + rawCors := d.Get("cors_rule").([]interface{}) + + if len(rawCors) == 0 { + // Delete CORS + log.Printf("[DEBUG] S3 bucket: %s, delete CORS", bucket) + _, err := s3conn.DeleteBucketCors(&s3.DeleteBucketCorsInput{ + Bucket: aws.String(bucket), + }) + if err != nil { + return fmt.Errorf("Error deleting S3 CORS: %s", err) + } + } else { + // Put CORS + rules := make([]*s3.CORSRule, 0, len(rawCors)) + for _, cors := range rawCors { + corsMap := cors.(map[string]interface{}) + r := &s3.CORSRule{} + for k, v := range corsMap { + log.Printf("[DEBUG] S3 bucket: %s, put CORS: %#v, %#v", bucket, k, v) + if k == "max_age_seconds" { + r.MaxAgeSeconds = aws.Int64(int64(v.(int))) + } else { + vMap := make([]*string, len(v.([]interface{}))) + for i, vv := range v.([]interface{}) { + str := vv.(string) + vMap[i] = aws.String(str) + } + switch k { + case "allowed_headers": + r.AllowedHeaders = vMap + case "allowed_methods": + r.AllowedMethods = vMap + case "allowed_origins": + r.AllowedOrigins = vMap + case "expose_headers": + r.ExposeHeaders = vMap + } + } + } + rules = append(rules, r) + } + corsInput := &s3.PutBucketCorsInput{ + Bucket: aws.String(bucket), + CORSConfiguration: &s3.CORSConfiguration{ + CORSRules: rules, + }, + } + log.Printf("[DEBUG] S3 bucket: %s, put CORS: %#v", bucket, corsInput) + _, err := s3conn.PutBucketCors(corsInput) + if err != nil { + return fmt.Errorf("Error putting S3 CORS: %s", err) + } + } + + return nil +} + func resourceAwsS3BucketWebsiteUpdate(s3conn *s3.S3, d *schema.ResourceData) error { ws := d.Get("website").([]interface{}) diff --git a/builtin/providers/aws/resource_aws_s3_bucket_test.go b/builtin/providers/aws/resource_aws_s3_bucket_test.go index e494816b3..4a969365a 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket_test.go +++ b/builtin/providers/aws/resource_aws_s3_bucket_test.go @@ -188,6 +188,34 @@ func TestAccAWSS3Bucket_Versioning(t *testing.T) { }) } +func TestAccAWSS3Bucket_Cors(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSS3BucketConfigWithCORS, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"), + testAccCheckAWSS3BucketCors( + "aws_s3_bucket.bucket", + []*s3.CORSRule{ + &s3.CORSRule{ + AllowedHeaders: []*string{aws.String("*")}, + AllowedMethods: []*string{aws.String("PUT"), aws.String("POST")}, + AllowedOrigins: []*string{aws.String("https://www.example.com")}, + ExposeHeaders: []*string{aws.String("x-amz-server-side-encryption"), aws.String("ETag")}, + MaxAgeSeconds: aws.Int64(3000), + }, + }, + ), + ), + }, + }, + }) +} + func testAccCheckAWSS3BucketDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).s3conn @@ -370,6 +398,26 @@ func testAccCheckAWSS3BucketVersioning(n string, versioningStatus string) resour return nil } } +func testAccCheckAWSS3BucketCors(n string, corsRules []*s3.CORSRule) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, _ := s.RootModule().Resources[n] + conn := testAccProvider.Meta().(*AWSClient).s3conn + + out, err := conn.GetBucketCors(&s3.GetBucketCorsInput{ + Bucket: aws.String(rs.Primary.ID), + }) + + if err != nil { + return fmt.Errorf("GetBucketCors error: %v", err) + } + + if !reflect.DeepEqual(out.CORSRules, corsRules) { + return fmt.Errorf("bad error cors rule, expected: %v, got %v", corsRules, out.CORSRules) + } + + return nil + } +} // These need a bit of randomness as the name can only be used once globally // within AWS @@ -452,3 +500,17 @@ resource "aws_s3_bucket" "bucket" { } } `, randInt) + +var testAccAWSS3BucketConfigWithCORS = fmt.Sprintf(` +resource "aws_s3_bucket" "bucket" { + bucket = "tf-test-bucket-%d" + acl = "public-read" + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["PUT","POST"] + allowed_origins = ["https://www.example.com"] + expose_headers = ["x-amz-server-side-encryption","ETag"] + max_age_seconds = 3000 + } +} +`, randInt) diff --git a/website/source/docs/providers/aws/r/s3_bucket.html.markdown b/website/source/docs/providers/aws/r/s3_bucket.html.markdown index 011f73347..da008053c 100644 --- a/website/source/docs/providers/aws/r/s3_bucket.html.markdown +++ b/website/source/docs/providers/aws/r/s3_bucket.html.markdown @@ -41,6 +41,23 @@ resource "aws_s3_bucket" "b" { } ``` +### Using CORS + +``` +resource "aws_s3_bucket" "b" { + bucket = "s3-website-test.hashicorp.com" + acl = "public-read" + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["PUT","POST"] + allowed_origins = ["https://s3-website-test.hashicorp.com"] + expose_headers = ["ETag"] + max_age_seconds = 3000 + } +} +``` + ### Using versioning ``` @@ -64,6 +81,7 @@ The following arguments are supported: * `tags` - (Optional) A mapping of tags to assign to the bucket. * `force_destroy` - (Optional, Default:false ) A boolean that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are *not* recoverable. * `website` - (Optional) A website object (documented below). +* `cors_rule` - (Optional) A rule of [Cross-Origin Resource Sharing](http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html) (documented below). * `versioning` - (Optional) A state of [versioning](http://docs.aws.amazon.com/AmazonS3/latest/dev/Versioning.html) (documented below) The website object supports the following: @@ -72,6 +90,14 @@ The website object supports the following: * `error_document` - (Optional) An absolute path to the document to return in case of a 4XX error. * `redirect_all_requests_to` - (Optional) A hostname to redirect all website requests for this bucket to. +The CORS supports the following: + +* `allowed_headers` (Optional) Specifies which headers are allowed. +* `allowed_methods` (Required) Specifies which methods are allowed. Can be `GET`, `PUT`, `POST`, `DELETE` or `HEAD`. +* `allowed_origins` (Required) Specifies which origins are allowed. +* `expose_headers` (Optional) Specifies expose header in the response. +* `max_age_seconds` (Optional) Specifies time in seconds that browser can cache the response for a preflight request. + The versioning supports the following: * `enabled` - (Optional) Enable versioning. Once you version-enable a bucket, it can never return to an unversioned state. You can, however, suspend versioning on that bucket. From 122790d32bd23fbe0c91e9bb1dd0bce19f1ec8b5 Mon Sep 17 00:00:00 2001 From: Kazunori Kojima Date: Wed, 28 Oct 2015 09:19:37 +0900 Subject: [PATCH 2/2] Add check errors on reading CORS rules --- builtin/providers/aws/resource_aws_s3_bucket.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_s3_bucket.go b/builtin/providers/aws/resource_aws_s3_bucket.go index 93105ec51..3c284370f 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket.go +++ b/builtin/providers/aws/resource_aws_s3_bucket.go @@ -276,7 +276,9 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error { rule["max_age_seconds"] = ruleObject.MaxAgeSeconds rules = append(rules, rule) } - d.Set("cors_rule", rules) + if err := d.Set("cors_rule", rules); err != nil { + return fmt.Errorf("error reading S3 bucket \"%s\" CORS rules: %s", d.Id(), err) + } } // Read the website configuration