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

provider/aws: Add CORS settings to S3 bucket #3387

Merged
merged 2 commits into from Oct 28, 2015
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
117 changes: 117 additions & 0 deletions builtin/providers/aws/resource_aws_s3_bucket.go
Expand Up @@ -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,
Expand Down Expand Up @@ -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") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we remove the CORs rules if the website is removed?

if err := resourceAwsS3BucketWebsiteUpdate(s3conn, d); err != nil {
return err
Expand Down Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check for errors here? We've found that setting non-primitives, there can be an error and it's best we inspect/log that

if err := d.Set("cors_rule", rules); err != nil {
  //log
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

}

// Read the website configuration
ws, err := s3conn.GetBucketWebsite(&s3.GetBucketWebsiteInput{
Bucket: aws.String(d.Id()),
Expand Down Expand Up @@ -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{})

Expand Down
62 changes: 62 additions & 0 deletions builtin/providers/aws/resource_aws_s3_bucket_test.go
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
26 changes: 26 additions & 0 deletions website/source/docs/providers/aws/r/s3_bucket.html.markdown
Expand Up @@ -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

```
Expand All @@ -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:
Expand All @@ -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.
Expand Down