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

Spaces: Add CORS Configuration Support #1021

Merged
merged 8 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions digitalocean/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func Provider() *schema.Provider {
"digitalocean_reserved_ip": reservedip.ResourceDigitalOceanReservedIP(),
"digitalocean_reserved_ip_assignment": reservedip.ResourceDigitalOceanReservedIPAssignment(),
"digitalocean_spaces_bucket": spaces.ResourceDigitalOceanBucket(),
"digitalocean_spaces_bucket_cors_configuration": spaces.ResourceDigitalOceanBucketCorsConfiguration(),
"digitalocean_spaces_bucket_object": spaces.ResourceDigitalOceanSpacesBucketObject(),
"digitalocean_spaces_bucket_policy": spaces.ResourceDigitalOceanSpacesBucketPolicy(),
"digitalocean_ssh_key": sshkey.ResourceDigitalOceanSSHKey(),
Expand Down
1 change: 1 addition & 0 deletions digitalocean/spaces/resource_spaces_bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func ResourceDigitalOceanBucket() *schema.Resource {
Type: schema.TypeList,
Optional: true,
Description: "A container holding a list of elements describing allowed methods for a specific origin.",
Deprecated: "Terraform will only perform drift detection if a configuration value is provided. Use the resource `digitalocean_spaces_bucket_cors_configuration` instead.",
Copy link
Member Author

Choose a reason for hiding this comment

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

Adding deprecation flag to encourage users to use digitalocean_spaces_bucket_cors_configuration. AWS deprecated this to add more flexibility with bucket creations:
hashicorp/terraform-provider-aws#4418

Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"allowed_methods": {
Expand Down
287 changes: 287 additions & 0 deletions digitalocean/spaces/resource_spaces_bucket_cors_configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
package spaces

import (
"context"
"log"

"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/digitalocean/terraform-provider-digitalocean/digitalocean/config"
"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"
)

func ResourceDigitalOceanBucketCorsConfiguration() *schema.Resource {
return &schema.Resource{
CreateContext: resourceDigitalOceanBucketCorsConfigurationCreate,
ReadContext: resourceDigitalOceanBucketCorsConfigurationRead,
UpdateContext: resourceDigitalOceanBucketCorsConfigurationUpdate,
DeleteContext: resourceBucketCorsConfigurationDelete,
Importer: &schema.ResourceImporter{
State: resourceDigitalOceanBucketImport,
},

Schema: map[string]*schema.Schema{
"bucket": {
Type: schema.TypeString,
Required: true,
Description: "Bucket ID",
},
"region": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice(SpacesRegions, true),
},
"cors_rule": {
Type: schema.TypeSet,
Required: true,
MaxItems: 100,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
danaelhe marked this conversation as resolved.
Show resolved Hide resolved
"allowed_headers": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"allowed_methods": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"allowed_origins": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"expose_headers": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"id": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringLenBetween(0, 255),
},
"max_age_seconds": {
Type: schema.TypeInt,
Optional: true,
},
},
},
},
},
}
}

func resourceDigitalOceanBucketCorsConfigurationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn, err := s3connFromSpacesBucketCorsResourceData(d, meta)
if err != nil {
return diag.Errorf("Error occurred while creating new Spaces bucket policy: %s", err)
}

bucket := d.Get("bucket").(string)

input := &s3.PutBucketCorsInput{
Bucket: aws.String(bucket),
CORSConfiguration: &s3.CORSConfiguration{
CORSRules: expandBucketCorsConfigurationCorsRules(d.Get("cors_rule").(*schema.Set).List()),
},
}

log.Printf("[DEBUG] Trying to configure CORS for Spaces bucket: %s", bucket)
_, err = conn.PutBucketCorsWithContext(ctx, input)

if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoSuchKey" {
return diag.Errorf("Unable to configure CORS for Spaces bucket because the bucket does not exist: '%s'", bucket)
}
return diag.Errorf("Error occurred while configuring CORS for Spaces bucket: %s", err)
}

d.SetId(bucket)
return resourceDigitalOceanBucketCorsConfigurationRead(ctx, d, meta)
}

func resourceDigitalOceanBucketCorsConfigurationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn, err := s3connFromSpacesBucketCorsResourceData(d, meta)
if err != nil {
return diag.Errorf("Error occurred while fetching Spaces bucket CORS configuration: %s", err)
}

log.Printf("[DEBUG] Trying to fetch Spaces bucket CORS configuration for bucket: %s", d.Id())
response, err := conn.GetBucketCorsWithContext(ctx, &s3.GetBucketCorsInput{
Bucket: aws.String(d.Id()),
})

if err != nil {
return diag.Errorf("Error occurred while fetching Spaces bucket CORS configuration: %s", err)
}

d.Set("bucket", d.Id())

if err := d.Set("cors_rule", flattenBucketCorsConfigurationCorsRules(response.CORSRules)); err != nil {
return diag.Errorf("setting cors_rule: %s", err)
}

return nil
}

func resourceDigitalOceanBucketCorsConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return resourceDigitalOceanBucketCorsConfigurationCreate(ctx, d, meta)
}

func resourceBucketCorsConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn, err := s3connFromSpacesBucketCorsResourceData(d, meta)
if err != nil {
return diag.Errorf("Error occurred while deleting Spaces bucket CORS configuration: %s", err)
}

bucket := d.Id()

log.Printf("[DEBUG] Trying to delete Spaces bucket CORS Configuration for bucket: %s", d.Id())
_, err = conn.DeleteBucketCorsWithContext(ctx, &s3.DeleteBucketCorsInput{
Bucket: aws.String(bucket),
})

if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "BucketDeleted" {
return diag.Errorf("Unable to remove Spaces bucket CORS configuration because bucket '%s' is already deleted", bucket)
}
return diag.Errorf("Error occurred while deleting Spaces Bucket CORS configuration: %s", err)
}
return nil

}

func s3connFromSpacesBucketCorsResourceData(d *schema.ResourceData, meta interface{}) (*s3.S3, error) {
region := d.Get("region").(string)

client, err := meta.(*config.CombinedConfig).SpacesClient(region)
if err != nil {
return nil, err
}

svc := s3.New(client)
return svc, nil
}

func flattenBucketCorsConfigurationCorsRules(rules []*s3.CORSRule) []interface{} {
var results []interface{}

for _, rule := range rules {
if rule == nil {
continue
}

m := make(map[string]interface{})

if len(rule.AllowedHeaders) > 0 {
m["allowed_headers"] = flattenStringSet(rule.AllowedHeaders)
}

if len(rule.AllowedMethods) > 0 {
m["allowed_methods"] = flattenStringSet(rule.AllowedMethods)
}

if len(rule.AllowedOrigins) > 0 {
m["allowed_origins"] = flattenStringSet(rule.AllowedOrigins)
}

if len(rule.ExposeHeaders) > 0 {
m["expose_headers"] = flattenStringSet(rule.ExposeHeaders)
}

if rule.ID != nil {
m["id"] = aws.StringValue(rule.ID)
}

if rule.MaxAgeSeconds != nil {
m["max_age_seconds"] = aws.Int64Value(rule.MaxAgeSeconds)
}

results = append(results, m)
}

return results
}

func flattenStringSet(list []*string) *schema.Set {
return schema.NewSet(schema.HashString, flattenStringList(list)) // nosemgrep:ci.helper-schema-Set-extraneous-NewSet-with-FlattenStringList
}

// flattenStringList takes list of pointers to strings. Expand to an array
// of raw strings and returns a []interface{}
// to keep compatibility w/ schema.NewSetschema.NewSet
func flattenStringList(list []*string) []interface{} {
vs := make([]interface{}, 0, len(list))
for _, v := range list {
vs = append(vs, *v)
}
return vs
}

func expandBucketCorsConfigurationCorsRules(l []interface{}) []*s3.CORSRule {
if len(l) == 0 {
return nil
}

var rules []*s3.CORSRule

for _, tfMapRaw := range l {
tfMap, ok := tfMapRaw.(map[string]interface{})
if !ok {
continue
}

rule := &s3.CORSRule{}

if v, ok := tfMap["allowed_headers"].(*schema.Set); ok && v.Len() > 0 {
rule.AllowedHeaders = expandStringSet(v)
}

if v, ok := tfMap["allowed_methods"].(*schema.Set); ok && v.Len() > 0 {
rule.AllowedMethods = expandStringSet(v)
}

if v, ok := tfMap["allowed_origins"].(*schema.Set); ok && v.Len() > 0 {
rule.AllowedOrigins = expandStringSet(v)
}

if v, ok := tfMap["expose_headers"].(*schema.Set); ok && v.Len() > 0 {
rule.ExposeHeaders = expandStringSet(v)
}

if v, ok := tfMap["id"].(string); ok && v != "" {
rule.ID = aws.String(v)
}

if v, ok := tfMap["max_age_seconds"].(int); ok {
rule.MaxAgeSeconds = aws.Int64(int64(v))
}

rules = append(rules, rule)
}

return rules
}

// expandStringSet takes the result of schema.Set of strings and returns a []*string
func expandStringSet(configured *schema.Set) []*string {
return expandStringList(configured.List()) // nosemgrep:ci.helper-schema-Set-extraneous-ExpandStringList-with-List
}

// ExpandStringList the result of flatmap.Expand for an array of strings
// and returns a []*string. Empty strings are skipped.
func expandStringList(configured []interface{}) []*string {
vs := make([]*string, 0, len(configured))
for _, v := range configured {
if v, ok := v.(string); ok && v != "" { // v != "" may not do anything since in []interface{}, empty string will be nil so !ok
vs = append(vs, aws.String(v))
}
}
return vs
}
Loading
Loading