Skip to content

Commit

Permalink
feat(object): add support for CORS in object_bucket (#654)
Browse files Browse the repository at this point in the history
  • Loading branch information
remyleone committed Dec 11, 2020
1 parent da4d55e commit 28142bd
Show file tree
Hide file tree
Showing 7 changed files with 1,532 additions and 104 deletions.
14 changes: 14 additions & 0 deletions docs/resources/object_bucket.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ The following arguments are supported:
* `tags` - (Optional) A list of tags (key / value) for the bucket.
* `acl` - (Optional) The [canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) you want to apply to the bucket.
* `region` - (Optional) The [region](https://developers.scaleway.com/en/quickstart/#region-definition) in which the bucket should be created.
* `versioning` - (Optional) A state of [versioning](https://docs.aws.amazon.com/AmazonS3/latest/dev/Versioning.html) (documented below)
* `cors_rule` - (Optional) A rule of [Cross-Origin Resource Sharing](https://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html) (documented below).

The `CORS` object 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` object 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.

## Attributes Reference

Expand Down
61 changes: 58 additions & 3 deletions scaleway/helpers_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,67 @@ func expandObjectBucketVersioning(v []interface{}) *s3.VersioningConfiguration {
c := v[0].(map[string]interface{})

if c["enabled"].(bool) {
vc.Status = aws.String(s3.BucketVersioningStatusEnabled)
vc.Status = scw.StringPtr(s3.BucketVersioningStatusEnabled)
} else {
vc.Status = aws.String(s3.BucketVersioningStatusSuspended)
vc.Status = scw.StringPtr(s3.BucketVersioningStatusSuspended)
}
} else {
vc.Status = aws.String(s3.BucketVersioningStatusSuspended)
vc.Status = scw.StringPtr(s3.BucketVersioningStatusSuspended)
}
return vc
}

func flattenBucketCORS(corsResponse interface{}) []map[string]interface{} {
corsRules := make([]map[string]interface{}, 0)
if cors, ok := corsResponse.(*s3.GetBucketCorsOutput); ok && len(cors.CORSRules) > 0 {
corsRules = make([]map[string]interface{}, 0, len(cors.CORSRules))
for _, ruleObject := range cors.CORSRules {
rule := make(map[string]interface{})
rule["allowed_headers"] = flattenSliceStringPtr(ruleObject.AllowedHeaders)
rule["allowed_methods"] = flattenSliceStringPtr(ruleObject.AllowedMethods)
rule["allowed_origins"] = flattenSliceStringPtr(ruleObject.AllowedOrigins)
// Both the "ExposeHeaders" and "MaxAgeSeconds" might not be set.
if ruleObject.AllowedOrigins != nil {
rule["expose_headers"] = flattenSliceStringPtr(ruleObject.ExposeHeaders)
}
if ruleObject.MaxAgeSeconds != nil {
rule["max_age_seconds"] = int(*ruleObject.MaxAgeSeconds)
}
corsRules = append(corsRules, rule)
}
}
return corsRules
}

func expandBucketCORS(rawCors []interface{}, bucket string) []*s3.CORSRule {
rules := make([]*s3.CORSRule, 0, len(rawCors))
for _, cors := range rawCors {
corsMap := cors.(map[string]interface{})
r := &s3.CORSRule{}
for k, v := range corsMap {
l.Debugf("S3 bucket: %s, put CORS: %#v, %#v", bucket, k, v)
if k == "max_age_seconds" {
r.MaxAgeSeconds = scw.Int64Ptr(int64(v.(int)))
} else {
vMap := make([]*string, len(v.([]interface{})))
for i, vv := range v.([]interface{}) {
if str, ok := vv.(string); ok {
vMap[i] = scw.StringPtr(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)
}
return rules
}
188 changes: 137 additions & 51 deletions scaleway/resource_object_bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import (
"context"
"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/scaleway/scaleway-sdk-go/scw"
)

func resourceScalewayObjectBucket() *schema.Resource {
Expand Down Expand Up @@ -56,6 +56,38 @@ func resourceScalewayObjectBucket() *schema.Resource {
Description: "Endpoint of the bucket",
Computed: true,
},
"cors_rule": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"allowed_headers": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"allowed_methods": {
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"allowed_origins": {
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"expose_headers": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"max_age_seconds": {
Type: schema.TypeInt,
Optional: true,
},
},
},
},
"region": regionSchema(),
"versioning": {
Type: schema.TypeList,
Expand Down Expand Up @@ -86,8 +118,8 @@ func resourceScalewayObjectBucketCreate(ctx context.Context, d *schema.ResourceD
}

_, err = s3Client.CreateBucketWithContext(ctx, &s3.CreateBucketInput{
Bucket: aws.String(bucketName),
ACL: aws.String(acl),
Bucket: scw.StringPtr(bucketName),
ACL: scw.StringPtr(acl),
})
if err != nil {
return diag.FromErr(err)
Expand All @@ -97,7 +129,7 @@ func resourceScalewayObjectBucketCreate(ctx context.Context, d *schema.ResourceD

if len(tagsSet) > 0 {
_, err = s3Client.PutBucketTaggingWithContext(ctx, &s3.PutBucketTaggingInput{
Bucket: aws.String(bucketName),
Bucket: scw.StringPtr(bucketName),
Tagging: &s3.Tagging{
TagSet: tagsSet,
},
Expand All @@ -109,6 +141,54 @@ func resourceScalewayObjectBucketCreate(ctx context.Context, d *schema.ResourceD

d.SetId(newRegionalIDString(region, bucketName))

return resourceScalewayObjectBucketUpdate(ctx, d, meta)
}

func resourceScalewayObjectBucketUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
s3Client, _, bucketName, err := s3ClientWithRegionAndName(meta, d.Id())
if err != nil {
return diag.FromErr(err)
}

if d.HasChange("acl") {
acl := d.Get("acl").(string)

_, err := s3Client.PutBucketAclWithContext(ctx, &s3.PutBucketAclInput{
Bucket: scw.StringPtr(bucketName),
ACL: scw.StringPtr(acl),
})
if err != nil {
l.Errorf("Couldn't update bucket ACL: %s", err)
return diag.FromErr(fmt.Errorf("couldn't update bucket ACL: %s", err))
}
}

if d.HasChange("versioning") {
if err := resourceScalewayObjectBucketVersioningUpdate(ctx, s3Client, d); err != nil {
return diag.FromErr(err)
}
}

if d.HasChange("tags") {
tagsSet := expandObjectBucketTags(d.Get("tags"))

_, err = s3Client.PutBucketTaggingWithContext(ctx, &s3.PutBucketTaggingInput{
Bucket: scw.StringPtr(bucketName),
Tagging: &s3.Tagging{
TagSet: tagsSet,
},
})
if err != nil {
return diag.FromErr(err)
}
}

if d.HasChange("cors_rule") {
if err := resourceScalewayS3BucketCorsUpdate(ctx, s3Client, d); err != nil {
return diag.FromErr(err)
}
}

return resourceScalewayObjectBucketRead(ctx, d, meta)
}

Expand All @@ -130,7 +210,7 @@ func resourceScalewayObjectBucketRead(ctx context.Context, d *schema.ResourceDat
// AWS has the same issue: https://github.com/terraform-providers/terraform-provider-aws/issues/6193

_, err = s3Client.ListObjectsWithContext(ctx, &s3.ListObjectsInput{
Bucket: aws.String(bucketName),
Bucket: scw.StringPtr(bucketName),
})
if err != nil {
if s3err, ok := err.(awserr.Error); ok && s3err.Code() == s3.ErrCodeNoSuchBucket {
Expand All @@ -144,7 +224,7 @@ func resourceScalewayObjectBucketRead(ctx context.Context, d *schema.ResourceDat
var tagsSet []*s3.Tag

tagsResponse, err := s3Client.GetBucketTaggingWithContext(ctx, &s3.GetBucketTaggingInput{
Bucket: aws.String(bucketName),
Bucket: scw.StringPtr(bucketName),
})
if err != nil {
if s3err, ok := err.(awserr.Error); !ok || s3err.Code() != "NoSuchTagSet" {
Expand All @@ -158,9 +238,22 @@ func resourceScalewayObjectBucketRead(ctx context.Context, d *schema.ResourceDat

_ = d.Set("endpoint", objectBucketEndpointURL(bucketName, region))

// Read the CORS
corsResponse, err := s3Client.GetBucketCorsWithContext(ctx, &s3.GetBucketCorsInput{
Bucket: scw.StringPtr(bucketName),
})

if err != nil && !isS3Err(err, "NoSuchCORSConfiguration", "") {
return diag.FromErr(fmt.Errorf("error getting S3 Bucket CORS configuration: %s", err))
}

_ = d.Set("cors_rule", flattenBucketCORS(corsResponse))

_ = d.Set("endpoint", fmt.Sprintf("https://%s.s3.%s.scw.cloud", bucketName, region))

// Read the versioning configuration
versioningResponse, err := s3Client.GetBucketVersioningWithContext(ctx, &s3.GetBucketVersioningInput{
Bucket: aws.String(bucketName),
Bucket: scw.StringPtr(bucketName),
})
if err != nil {
return diag.FromErr(err)
Expand All @@ -170,56 +263,14 @@ func resourceScalewayObjectBucketRead(ctx context.Context, d *schema.ResourceDat
return nil
}

func resourceScalewayObjectBucketUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
s3Client, _, bucketName, err := s3ClientWithRegionAndName(meta, d.Id())
if err != nil {
return diag.FromErr(err)
}

if d.HasChange("acl") {
acl := d.Get("acl").(string)

_, err := s3Client.PutBucketAclWithContext(ctx, &s3.PutBucketAclInput{
Bucket: aws.String(bucketName),
ACL: aws.String(acl),
})
if err != nil {
l.Errorf("Couldn't update bucket ACL: %s", err)
return diag.FromErr(fmt.Errorf("couldn't update bucket ACL: %s", err))
}
}

if d.HasChange("versioning") {
if err := resourceScalewayObjectBucketVersioningUpdate(ctx, s3Client, d); err != nil {
return diag.FromErr(err)
}
}

if d.HasChange("tags") {
tagsSet := expandObjectBucketTags(d.Get("tags"))

_, err = s3Client.PutBucketTaggingWithContext(ctx, &s3.PutBucketTaggingInput{
Bucket: aws.String(bucketName),
Tagging: &s3.Tagging{
TagSet: tagsSet,
},
})
if err != nil {
return diag.FromErr(err)
}
}

return resourceScalewayObjectBucketRead(ctx, d, meta)
}

func resourceScalewayObjectBucketDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
s3Client, _, bucketName, err := s3ClientWithRegionAndName(meta, d.Id())
if err != nil {
return diag.FromErr(err)
}

_, err = s3Client.DeleteBucketWithContext(ctx, &s3.DeleteBucketInput{
Bucket: aws.String(bucketName),
Bucket: scw.StringPtr(bucketName),
})
if err != nil {
return diag.FromErr(err)
Expand All @@ -234,7 +285,7 @@ func resourceScalewayObjectBucketVersioningUpdate(ctx context.Context, s3conn *s
vc := expandObjectBucketVersioning(v)

i := &s3.PutBucketVersioningInput{
Bucket: aws.String(bucketName),
Bucket: scw.StringPtr(bucketName),
VersioningConfiguration: vc,
}
l.Debugf("S3 put bucket versioning: %#v", i)
Expand All @@ -246,3 +297,38 @@ func resourceScalewayObjectBucketVersioningUpdate(ctx context.Context, s3conn *s

return nil
}

func resourceScalewayS3BucketCorsUpdate(ctx context.Context, s3conn *s3.S3, d *schema.ResourceData) error {
bucketName := d.Get("name").(string)
rawCors := d.Get("cors_rule").([]interface{})

if len(rawCors) == 0 {
// Delete CORS
l.Debugf("S3 bucket: %s, delete CORS", bucketName)

_, err := s3conn.DeleteBucketCorsWithContext(ctx, &s3.DeleteBucketCorsInput{
Bucket: scw.StringPtr(bucketName),
})

if err != nil {
return fmt.Errorf("error deleting S3 CORS: %s", err)
}
} else {
// Put CORS
rules := expandBucketCORS(rawCors, bucketName)
corsInput := &s3.PutBucketCorsInput{
Bucket: scw.StringPtr(bucketName),
CORSConfiguration: &s3.CORSConfiguration{
CORSRules: rules,
},
}
l.Debugf("S3 bucket: %s, put CORS: %#v", bucketName, corsInput)

_, err := s3conn.PutBucketCorsWithContext(ctx, corsInput)
if err != nil {
return fmt.Errorf("error putting S3 CORS: %s", err)
}
}

return nil
}
Loading

0 comments on commit 28142bd

Please sign in to comment.