Skip to content

Commit 28142bd

Browse files
authored
feat(object): add support for CORS in object_bucket (#654)
1 parent da4d55e commit 28142bd

7 files changed

+1532
-104
lines changed

docs/resources/object_bucket.md

+14
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,20 @@ The following arguments are supported:
2828
* `tags` - (Optional) A list of tags (key / value) for the bucket.
2929
* `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.
3030
* `region` - (Optional) The [region](https://developers.scaleway.com/en/quickstart/#region-definition) in which the bucket should be created.
31+
* `versioning` - (Optional) A state of [versioning](https://docs.aws.amazon.com/AmazonS3/latest/dev/Versioning.html) (documented below)
32+
* `cors_rule` - (Optional) A rule of [Cross-Origin Resource Sharing](https://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html) (documented below).
33+
34+
The `CORS` object supports the following:
35+
36+
* `allowed_headers` (Optional) Specifies which headers are allowed.
37+
* `allowed_methods` (Required) Specifies which methods are allowed. Can be `GET`, `PUT`, `POST`, `DELETE` or `HEAD`.
38+
* `allowed_origins` (Required) Specifies which origins are allowed.
39+
* `expose_headers` (Optional) Specifies expose header in the response.
40+
* `max_age_seconds` (Optional) Specifies time in seconds that browser can cache the response for a preflight request.
41+
42+
The `versioning` object supports the following:
43+
44+
* `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.
3145

3246
## Attributes Reference
3347

scaleway/helpers_object.go

+58-3
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,67 @@ func expandObjectBucketVersioning(v []interface{}) *s3.VersioningConfiguration {
144144
c := v[0].(map[string]interface{})
145145

146146
if c["enabled"].(bool) {
147-
vc.Status = aws.String(s3.BucketVersioningStatusEnabled)
147+
vc.Status = scw.StringPtr(s3.BucketVersioningStatusEnabled)
148148
} else {
149-
vc.Status = aws.String(s3.BucketVersioningStatusSuspended)
149+
vc.Status = scw.StringPtr(s3.BucketVersioningStatusSuspended)
150150
}
151151
} else {
152-
vc.Status = aws.String(s3.BucketVersioningStatusSuspended)
152+
vc.Status = scw.StringPtr(s3.BucketVersioningStatusSuspended)
153153
}
154154
return vc
155155
}
156+
157+
func flattenBucketCORS(corsResponse interface{}) []map[string]interface{} {
158+
corsRules := make([]map[string]interface{}, 0)
159+
if cors, ok := corsResponse.(*s3.GetBucketCorsOutput); ok && len(cors.CORSRules) > 0 {
160+
corsRules = make([]map[string]interface{}, 0, len(cors.CORSRules))
161+
for _, ruleObject := range cors.CORSRules {
162+
rule := make(map[string]interface{})
163+
rule["allowed_headers"] = flattenSliceStringPtr(ruleObject.AllowedHeaders)
164+
rule["allowed_methods"] = flattenSliceStringPtr(ruleObject.AllowedMethods)
165+
rule["allowed_origins"] = flattenSliceStringPtr(ruleObject.AllowedOrigins)
166+
// Both the "ExposeHeaders" and "MaxAgeSeconds" might not be set.
167+
if ruleObject.AllowedOrigins != nil {
168+
rule["expose_headers"] = flattenSliceStringPtr(ruleObject.ExposeHeaders)
169+
}
170+
if ruleObject.MaxAgeSeconds != nil {
171+
rule["max_age_seconds"] = int(*ruleObject.MaxAgeSeconds)
172+
}
173+
corsRules = append(corsRules, rule)
174+
}
175+
}
176+
return corsRules
177+
}
178+
179+
func expandBucketCORS(rawCors []interface{}, bucket string) []*s3.CORSRule {
180+
rules := make([]*s3.CORSRule, 0, len(rawCors))
181+
for _, cors := range rawCors {
182+
corsMap := cors.(map[string]interface{})
183+
r := &s3.CORSRule{}
184+
for k, v := range corsMap {
185+
l.Debugf("S3 bucket: %s, put CORS: %#v, %#v", bucket, k, v)
186+
if k == "max_age_seconds" {
187+
r.MaxAgeSeconds = scw.Int64Ptr(int64(v.(int)))
188+
} else {
189+
vMap := make([]*string, len(v.([]interface{})))
190+
for i, vv := range v.([]interface{}) {
191+
if str, ok := vv.(string); ok {
192+
vMap[i] = scw.StringPtr(str)
193+
}
194+
}
195+
switch k {
196+
case "allowed_headers":
197+
r.AllowedHeaders = vMap
198+
case "allowed_methods":
199+
r.AllowedMethods = vMap
200+
case "allowed_origins":
201+
r.AllowedOrigins = vMap
202+
case "expose_headers":
203+
r.ExposeHeaders = vMap
204+
}
205+
}
206+
}
207+
rules = append(rules, r)
208+
}
209+
return rules
210+
}

scaleway/resource_object_bucket.go

+137-51
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import (
44
"context"
55
"fmt"
66

7-
"github.com/aws/aws-sdk-go/aws"
87
"github.com/aws/aws-sdk-go/aws/awserr"
98
"github.com/aws/aws-sdk-go/service/s3"
109
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1110
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1211
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
12+
"github.com/scaleway/scaleway-sdk-go/scw"
1313
)
1414

1515
func resourceScalewayObjectBucket() *schema.Resource {
@@ -56,6 +56,38 @@ func resourceScalewayObjectBucket() *schema.Resource {
5656
Description: "Endpoint of the bucket",
5757
Computed: true,
5858
},
59+
"cors_rule": {
60+
Type: schema.TypeList,
61+
Optional: true,
62+
Elem: &schema.Resource{
63+
Schema: map[string]*schema.Schema{
64+
"allowed_headers": {
65+
Type: schema.TypeList,
66+
Optional: true,
67+
Elem: &schema.Schema{Type: schema.TypeString},
68+
},
69+
"allowed_methods": {
70+
Type: schema.TypeList,
71+
Required: true,
72+
Elem: &schema.Schema{Type: schema.TypeString},
73+
},
74+
"allowed_origins": {
75+
Type: schema.TypeList,
76+
Required: true,
77+
Elem: &schema.Schema{Type: schema.TypeString},
78+
},
79+
"expose_headers": {
80+
Type: schema.TypeList,
81+
Optional: true,
82+
Elem: &schema.Schema{Type: schema.TypeString},
83+
},
84+
"max_age_seconds": {
85+
Type: schema.TypeInt,
86+
Optional: true,
87+
},
88+
},
89+
},
90+
},
5991
"region": regionSchema(),
6092
"versioning": {
6193
Type: schema.TypeList,
@@ -86,8 +118,8 @@ func resourceScalewayObjectBucketCreate(ctx context.Context, d *schema.ResourceD
86118
}
87119

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

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

110142
d.SetId(newRegionalIDString(region, bucketName))
111143

144+
return resourceScalewayObjectBucketUpdate(ctx, d, meta)
145+
}
146+
147+
func resourceScalewayObjectBucketUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
148+
s3Client, _, bucketName, err := s3ClientWithRegionAndName(meta, d.Id())
149+
if err != nil {
150+
return diag.FromErr(err)
151+
}
152+
153+
if d.HasChange("acl") {
154+
acl := d.Get("acl").(string)
155+
156+
_, err := s3Client.PutBucketAclWithContext(ctx, &s3.PutBucketAclInput{
157+
Bucket: scw.StringPtr(bucketName),
158+
ACL: scw.StringPtr(acl),
159+
})
160+
if err != nil {
161+
l.Errorf("Couldn't update bucket ACL: %s", err)
162+
return diag.FromErr(fmt.Errorf("couldn't update bucket ACL: %s", err))
163+
}
164+
}
165+
166+
if d.HasChange("versioning") {
167+
if err := resourceScalewayObjectBucketVersioningUpdate(ctx, s3Client, d); err != nil {
168+
return diag.FromErr(err)
169+
}
170+
}
171+
172+
if d.HasChange("tags") {
173+
tagsSet := expandObjectBucketTags(d.Get("tags"))
174+
175+
_, err = s3Client.PutBucketTaggingWithContext(ctx, &s3.PutBucketTaggingInput{
176+
Bucket: scw.StringPtr(bucketName),
177+
Tagging: &s3.Tagging{
178+
TagSet: tagsSet,
179+
},
180+
})
181+
if err != nil {
182+
return diag.FromErr(err)
183+
}
184+
}
185+
186+
if d.HasChange("cors_rule") {
187+
if err := resourceScalewayS3BucketCorsUpdate(ctx, s3Client, d); err != nil {
188+
return diag.FromErr(err)
189+
}
190+
}
191+
112192
return resourceScalewayObjectBucketRead(ctx, d, meta)
113193
}
114194

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

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

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

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

241+
// Read the CORS
242+
corsResponse, err := s3Client.GetBucketCorsWithContext(ctx, &s3.GetBucketCorsInput{
243+
Bucket: scw.StringPtr(bucketName),
244+
})
245+
246+
if err != nil && !isS3Err(err, "NoSuchCORSConfiguration", "") {
247+
return diag.FromErr(fmt.Errorf("error getting S3 Bucket CORS configuration: %s", err))
248+
}
249+
250+
_ = d.Set("cors_rule", flattenBucketCORS(corsResponse))
251+
252+
_ = d.Set("endpoint", fmt.Sprintf("https://%s.s3.%s.scw.cloud", bucketName, region))
253+
161254
// Read the versioning configuration
162255
versioningResponse, err := s3Client.GetBucketVersioningWithContext(ctx, &s3.GetBucketVersioningInput{
163-
Bucket: aws.String(bucketName),
256+
Bucket: scw.StringPtr(bucketName),
164257
})
165258
if err != nil {
166259
return diag.FromErr(err)
@@ -170,56 +263,14 @@ func resourceScalewayObjectBucketRead(ctx context.Context, d *schema.ResourceDat
170263
return nil
171264
}
172265

173-
func resourceScalewayObjectBucketUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
174-
s3Client, _, bucketName, err := s3ClientWithRegionAndName(meta, d.Id())
175-
if err != nil {
176-
return diag.FromErr(err)
177-
}
178-
179-
if d.HasChange("acl") {
180-
acl := d.Get("acl").(string)
181-
182-
_, err := s3Client.PutBucketAclWithContext(ctx, &s3.PutBucketAclInput{
183-
Bucket: aws.String(bucketName),
184-
ACL: aws.String(acl),
185-
})
186-
if err != nil {
187-
l.Errorf("Couldn't update bucket ACL: %s", err)
188-
return diag.FromErr(fmt.Errorf("couldn't update bucket ACL: %s", err))
189-
}
190-
}
191-
192-
if d.HasChange("versioning") {
193-
if err := resourceScalewayObjectBucketVersioningUpdate(ctx, s3Client, d); err != nil {
194-
return diag.FromErr(err)
195-
}
196-
}
197-
198-
if d.HasChange("tags") {
199-
tagsSet := expandObjectBucketTags(d.Get("tags"))
200-
201-
_, err = s3Client.PutBucketTaggingWithContext(ctx, &s3.PutBucketTaggingInput{
202-
Bucket: aws.String(bucketName),
203-
Tagging: &s3.Tagging{
204-
TagSet: tagsSet,
205-
},
206-
})
207-
if err != nil {
208-
return diag.FromErr(err)
209-
}
210-
}
211-
212-
return resourceScalewayObjectBucketRead(ctx, d, meta)
213-
}
214-
215266
func resourceScalewayObjectBucketDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
216267
s3Client, _, bucketName, err := s3ClientWithRegionAndName(meta, d.Id())
217268
if err != nil {
218269
return diag.FromErr(err)
219270
}
220271

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

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

247298
return nil
248299
}
300+
301+
func resourceScalewayS3BucketCorsUpdate(ctx context.Context, s3conn *s3.S3, d *schema.ResourceData) error {
302+
bucketName := d.Get("name").(string)
303+
rawCors := d.Get("cors_rule").([]interface{})
304+
305+
if len(rawCors) == 0 {
306+
// Delete CORS
307+
l.Debugf("S3 bucket: %s, delete CORS", bucketName)
308+
309+
_, err := s3conn.DeleteBucketCorsWithContext(ctx, &s3.DeleteBucketCorsInput{
310+
Bucket: scw.StringPtr(bucketName),
311+
})
312+
313+
if err != nil {
314+
return fmt.Errorf("error deleting S3 CORS: %s", err)
315+
}
316+
} else {
317+
// Put CORS
318+
rules := expandBucketCORS(rawCors, bucketName)
319+
corsInput := &s3.PutBucketCorsInput{
320+
Bucket: scw.StringPtr(bucketName),
321+
CORSConfiguration: &s3.CORSConfiguration{
322+
CORSRules: rules,
323+
},
324+
}
325+
l.Debugf("S3 bucket: %s, put CORS: %#v", bucketName, corsInput)
326+
327+
_, err := s3conn.PutBucketCorsWithContext(ctx, corsInput)
328+
if err != nil {
329+
return fmt.Errorf("error putting S3 CORS: %s", err)
330+
}
331+
}
332+
333+
return nil
334+
}

0 commit comments

Comments
 (0)