From 2a6fdf8d8da8ec4d3106e82c40035b3adcfb8b98 Mon Sep 17 00:00:00 2001 From: danaelhe <42972711+danaelhe@users.noreply.github.com> Date: Mon, 21 Aug 2023 15:36:43 -0400 Subject: [PATCH 1/6] Spaces: Add CORS Coniguration Support --- digitalocean/provider.go | 1 + ...source_spaces_bucket_cors_configuration.go | 259 ++++++++++++++ ...e_spaces_bucket_cors_configuration_test.go | 328 ++++++++++++++++++ .../spaces_bucket_cors_configuration.md | 88 +++++ 4 files changed, 676 insertions(+) create mode 100644 digitalocean/spaces/resource_spaces_bucket_cors_configuration.go create mode 100644 digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go create mode 100644 docs/resources/spaces_bucket_cors_configuration.md diff --git a/digitalocean/provider.go b/digitalocean/provider.go index 1efc16f35..03e7c8100 100644 --- a/digitalocean/provider.go +++ b/digitalocean/provider.go @@ -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(), diff --git a/digitalocean/spaces/resource_spaces_bucket_cors_configuration.go b/digitalocean/spaces/resource_spaces_bucket_cors_configuration.go new file mode 100644 index 000000000..12049126d --- /dev/null +++ b/digitalocean/spaces/resource_spaces_bucket_cors_configuration.go @@ -0,0 +1,259 @@ +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.TypeList, + Optional: true, + Description: "A container holding a list of elements describing allowed methods for a specific origin.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allowed_methods": { + Type: schema.TypeList, + Required: true, + Description: "A list of HTTP methods (e.g. GET) which are allowed from the specified origin.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "allowed_origins": { + Type: schema.TypeList, + Required: true, + Description: "A list of hosts from which requests using the specified methods are allowed. A host may contain one wildcard (e.g. http://*.example.com).", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "allowed_headers": { + Type: schema.TypeList, + Optional: true, + Description: "A list of headers that will be included in the CORS preflight request's Access-Control-Request-Headers. A header may contain one wildcard (e.g. x-amz-*).", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "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) + rawCors := d.Get("cors_rule").([]interface{}) + + if len(rawCors) == 0 { + return diag.Errorf("Spaces bucket CORS must not be empty") + } + + input := &s3.PutBucketCorsInput{ + Bucket: aws.String(bucket), + CORSConfiguration: &s3.CORSConfiguration{ + CORSRules: expandBucketCorsConfigurationCorsRules(rawCors, bucket), + }, + } + + 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 resourceDigitalOceanBucketPolicyRead(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 expandBucketCorsConfigurationCorsRules(rawCors []interface{}, bucketName string) []*s3.CORSRule { + if len(rawCors) == 0 { + return nil + } + + 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] Spaces bucket: %s, put CORS: %#v, %#v", bucketName, 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) + } + return rules +} + +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 +} diff --git a/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go b/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go new file mode 100644 index 000000000..096d3fd35 --- /dev/null +++ b/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go @@ -0,0 +1,328 @@ +package spaces_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/acceptance" + "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +const ( + testAccDigitalOceanSpacesBucketCorsConfiguration_TestRegion = "nyc3" +) + +func TestAccDigitalOceanBucketCorsConfiguration_basic(t *testing.T) { + name := acceptance.RandomTestName() + ctx := context.Background() + region := testAccDigitalOceanSpacesBucketCorsConfiguration_TestRegion + resourceName := "digitalocean_spaces_bucket_cors_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: testAccCheckDigitalOceanSpacesBucketCorsConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSpacesBucketCORSConfigurationConfig_basic(name, region, "https://www.example.com"), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanSpacesBucketCorsConfigurationExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "cors_rule.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "cors_rule.*", map[string]string{ + "allowed_methods.#": "1", + "allowed_origins.#": "1", + }), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_methods.*", "PUT"), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_origins.*", "https://www.example.com"), + ), + }, + }, + }) +} + +func TestAccS3BucketCorsConfiguration_SingleRule(t *testing.T) { + resourceName := "digitalocean_spaces_bucket_cors_configuration.test" + rName := acceptance.RandomTestName() + ctx := context.Background() + region := testAccDigitalOceanSpacesBucketCorsConfiguration_TestRegion + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: testAccCheckDigitalOceanSpacesBucketCorsConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSpacesBucketCORSConfigurationConfig_completeSingleRule(rName, region, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanSpacesBucketCorsConfigurationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "digitalocean_spaces_bucket.test", "id"), + resource.TestCheckResourceAttr(resourceName, "cors_rule.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "cors_rule.*", map[string]string{ + "allowed_headers.#": "1", + "allowed_methods.#": "3", + "allowed_origins.#": "1", + "expose_headers.#": "1", + "id": rName, + "max_age_seconds": "3000", + }), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_headers.*", "*"), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_methods.*", "DELETE"), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_methods.*", "POST"), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_methods.*", "PUT"), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_origins.*", "https://www.example.com"), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.expose_headers.*", "ETag"), + ), + }, + }, + }) +} + +func TestAccS3BucketCorsConfiguration_MultipleRules(t *testing.T) { + resourceName := "digitalocean_spaces_bucket_cors_configuration.test" + rName := acceptance.RandomTestName() + ctx := context.Background() + region := testAccDigitalOceanSpacesBucketCorsConfiguration_TestRegion + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: testAccCheckDigitalOceanSpacesBucketCorsConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSpacesBucketCORSConfigurationConfig_multipleRules(rName, region, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanSpacesBucketCorsConfigurationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "digitalocean_spaces_bucket.test", "id"), + resource.TestCheckResourceAttr(resourceName, "cors_rule.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "cors_rule.*", map[string]string{ + "allowed_headers.#": "1", + "allowed_methods.#": "3", + "allowed_origins.#": "1", + }), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_headers.*", "*"), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_methods.*", "DELETE"), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_methods.*", "POST"), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_methods.*", "PUT"), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_origins.*", "https://www.example.com"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "cors_rule.*", map[string]string{ + "allowed_methods.#": "1", + "allowed_origins.#": "1", + }), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_methods.*", "GET"), + resource.TestCheckTypeSetElemAttr(resourceName, "cors_rule.*.allowed_origins.*", "*"), + ), + }, + }, + }) +} + +func TestAccS3BucketCorsConfiguration_update(t *testing.T) { + resourceName := "digitalocean_spaces_bucket_cors_configuration.test" + rName := acceptance.RandomTestName() + ctx := context.Background() + region := testAccDigitalOceanSpacesBucketCorsConfiguration_TestRegion + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: testAccCheckDigitalOceanSpacesBucketCorsConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSpacesBucketCORSConfigurationConfig_completeSingleRule(rName, region, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanSpacesBucketCorsConfigurationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "digitalocean_spaces_bucket.test", "id"), + resource.TestCheckResourceAttr(resourceName, "cors_rule.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "cors_rule.*", map[string]string{ + "allowed_headers.#": "1", + "allowed_methods.#": "3", + "allowed_origins.#": "1", + "expose_headers.#": "1", + "id": rName, + "max_age_seconds": "3000", + }), + ), + }, + { + Config: testAccSpacesBucketCORSConfigurationConfig_multipleRules(rName, region, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanSpacesBucketCorsConfigurationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "digitalocean_spaces_bucket.test", "id"), + resource.TestCheckResourceAttr(resourceName, "cors_rule.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "cors_rule.*", map[string]string{ + "allowed_headers.#": "1", + "allowed_methods.#": "3", + "allowed_origins.#": "1", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "cors_rule.*", map[string]string{ + "allowed_methods.#": "1", + "allowed_origins.#": "1", + }), + ), + }, + { + Config: testAccSpacesBucketCORSConfigurationConfig_basic(rName, region, "https://www.example.com"), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanSpacesBucketCorsConfigurationExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "cors_rule.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "cors_rule.*", map[string]string{ + "allowed_methods.#": "1", + "allowed_origins.#": "1", + }), + ), + }, + }, + }) +} + +func testAccGetS3CorsConfigurationConn() (*s3.S3, error) { + client, err := acceptance.TestAccProvider.Meta().(*config.CombinedConfig).SpacesClient(testAccDigitalOceanSpacesBucketCorsConfiguration_TestRegion) + if err != nil { + return nil, err + } + + s3conn := s3.New(client) + + return s3conn, nil +} + +func testAccCheckDigitalOceanSpacesBucketCorsConfigurationExists(ctx context.Context, resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not Found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("Resource (%s) ID not set", resourceName) + } + + s3conn, err := testAccGetS3CorsConfigurationConn() + if err != nil { + return err + } + + response, err := s3conn.GetBucketCorsWithContext(context.Background(), + &s3.GetBucketCorsInput{ + Bucket: aws.String(rs.Primary.Attributes["bucket"]), + }) + if err != nil { + return fmt.Errorf("S3Bucket CORs error: %s", err) + } + + if len(response.CORSRules) == 0 { + return fmt.Errorf("S3 Bucket CORS configuration (%s) not found", rs.Primary.ID) + } + return nil + } +} + +func testAccCheckDigitalOceanSpacesBucketCorsConfigurationDestroy(s *terraform.State) error { + s3conn, err := testAccGetS3CorsConfigurationConn() + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + switch rs.Type { + case "digitalocean_spaces_bucket_cors_configuration": + _, err := s3conn.GetBucketCorsWithContext(context.Background(), &s3.GetBucketCorsInput{ + Bucket: aws.String(rs.Primary.Attributes["bucket"]), + }) + if err == nil { + return fmt.Errorf("Spaces Bucket Cors Configuration still exists: %s", rs.Primary.ID) + } + + case "digitalocean_spaces_bucket": + _, err = s3conn.HeadBucket(&s3.HeadBucketInput{ + Bucket: aws.String(rs.Primary.ID), + }) + if err == nil { + return fmt.Errorf("Spaces Bucket still exists: %s", rs.Primary.ID) + } + + default: + continue + } + } + + return nil +} + +func testAccSpacesBucketCORSConfigurationConfig_basic(rName string, region string, origin string) string { + return fmt.Sprintf(` +resource "digitalocean_spaces_bucket" "foobar" { + name = %s + region = %s + } + +resource "digitalocean_spaces_bucket_cors_configuration" "test" { + bucket = digitalocean_spaces_bucket.foobar.id + region = "nyc3" + + cors_rule { + allowed_methods = ["PUT"] + allowed_origins = ["%s"] + } +} +`, rName, region, origin) +} + +func testAccSpacesBucketCORSConfigurationConfig_completeSingleRule(rName string, region string, Name string) string { + return fmt.Sprintf(` +resource "digitalocean_spaces_bucket" "foobar" { + region = "%s" + name = "%s" + force_destroy = true + } + +resource "digitalocean_spaces_bucket_cors_configuration" "test" { + bucket = digitalocean_spaces_bucket.foobar.id + region = "nyc3" + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["PUT", "POST", "DELETE"] + allowed_origins = ["https://www.example.com"] + expose_headers = ["ETag"] + id = %s + max_age_seconds = 3000 + } +} +`, rName, region, Name) +} + +func testAccSpacesBucketCORSConfigurationConfig_multipleRules(rName string, region string, Name string) string { + return fmt.Sprintf(` +resource "digitalocean_spaces_bucket" "foobar" { + region = "%s" + name = "%s" + force_destroy = true + } + +resource "digitalocean_spaces_bucket_cors_configuration" "test" { + bucket = digitalocean_spaces_bucket.foobar.id + region = "nyc3" + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["PUT", "POST", "DELETE"] + allowed_origins = ["https://www.example.com"] + expose_headers = ["ETag"] + id = %s + max_age_seconds = 3000 + } + + cors_rule { + allowed_methods = ["GET"] + allowed_origins = ["*"] + } +} +`, rName, region, Name) +} diff --git a/docs/resources/spaces_bucket_cors_configuration.md b/docs/resources/spaces_bucket_cors_configuration.md new file mode 100644 index 000000000..30285a5f7 --- /dev/null +++ b/docs/resources/spaces_bucket_cors_configuration.md @@ -0,0 +1,88 @@ +--- +page_title: "DigitalOcean: digitalocean_spaces_bucket_cors_configuration" +--- + +# digitalocean\_spaces\_cors_configuration + +Provides a CORS configuration resource for Spaces, DigitalOcean's object storage product. +The `digitalocean_spaces_bucket_cors_configuration` resource allows Terraform to to attach CORS configuration to Spaces. + +The [Spaces API](https://docs.digitalocean.com/reference/api/spaces-api/) was +designed to be interoperable with Amazon's AWS S3 API. This allows users to +interact with the service while using the tools they already know. Spaces +mirrors S3's authentication framework and requests to Spaces require a key pair +similar to Amazon's Access ID and Secret Key. + +The authentication requirement can be met by either setting the +`SPACES_ACCESS_KEY_ID` and `SPACES_SECRET_ACCESS_KEY` environment variables or +the provider's `spaces_access_id` and `spaces_secret_key` arguments to the +access ID and secret you generate via the DigitalOcean control panel. For +example: + +``` +provider "digitalocean" { + token = var.digitalocean_token + + spaces_access_id = var.access_id + spaces_secret_key = var.secret_key +} + +resource "digitalocean_spaces_bucket" "static-assets" { + # ... +} +``` + +For more information, See [An Introduction to DigitalOcean Spaces](https://www.digitalocean.com/community/tutorials/an-introduction-to-digitalocean-spaces) + +## Example Usage + +### Create a Key in a Spaces Bucket + +```hcl +resource "digitalocean_spaces_bucket" "foobar" { + name = "foobar" + region = "nyc3" +} + +resource "digitalocean_spaces_bucket_cors_configuration" "test" { + bucket = digitalocean_spaces_bucket.foobar.id + region = "nyc3" + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["PUT", "POST"] + allowed_origins = ["https://s3-website-test.hashicorp.com"] + expose_headers = ["ETag"] + max_age_seconds = 3000 + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `bucket` - (Required) The name of the bucket to which to apply the CORS configuration. +* `region` - (Required) The region where the bucket resides. +* `cors_rule` - (Required) Set of origins and methods (cross-origin access that you want to allow). See below. You can configure up to 100 rules. + +`cors_rule` supports the following: + +* `allowed_headers` - (Optional) Set of Headers that are specified in the Access-Control-Request-Headers header. +* `allowed_methods` - (Required) Set of HTTP methods that you allow the origin to execute. Valid values are GET, PUT, HEAD, POST, and DELETE. +* `allowed_origins` - (Required) Set of origins you want customers to be able to access the bucket from. +* `expose_headers` - (Optional) Set of headers in the response that you want customers to be able to access from their applications (for example, from a JavaScript XMLHttpRequest object). +* `id` - (Optional) Unique identifier for the rule. The value cannot be longer than 255 characters. +* `max_age_seconds` - (Optional) Time in seconds that your browser is to cache the preflight response for the specified resource. + +## Attributes Reference + +No additional attributes are exported. + +## Import + +Bucket policies can be imported using the `region` and `bucket` attributes (delimited by a comma): + +``` +terraform import digitalocean_spaces_bucket_cors_configuration.foobar `region`,`bucket` +``` From 4e141e05d1b25283598d74bbbd01164da9adef7c Mon Sep 17 00:00:00 2001 From: danaelhe <42972711+danaelhe@users.noreply.github.com> Date: Mon, 21 Aug 2023 16:17:30 -0400 Subject: [PATCH 2/6] terrafmt fmt --fmtcompat digitalocean/ --- ...e_spaces_bucket_cors_configuration_test.go | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go b/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go index 096d3fd35..0399ba156 100644 --- a/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go +++ b/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go @@ -258,9 +258,9 @@ func testAccCheckDigitalOceanSpacesBucketCorsConfigurationDestroy(s *terraform.S func testAccSpacesBucketCORSConfigurationConfig_basic(rName string, region string, origin string) string { return fmt.Sprintf(` resource "digitalocean_spaces_bucket" "foobar" { - name = %s - region = %s - } + name = %s + region = %s +} resource "digitalocean_spaces_bucket_cors_configuration" "test" { bucket = digitalocean_spaces_bucket.foobar.id @@ -277,14 +277,14 @@ resource "digitalocean_spaces_bucket_cors_configuration" "test" { func testAccSpacesBucketCORSConfigurationConfig_completeSingleRule(rName string, region string, Name string) string { return fmt.Sprintf(` resource "digitalocean_spaces_bucket" "foobar" { - region = "%s" - name = "%s" - force_destroy = true - } + region = "%s" + name = "%s" + force_destroy = true +} resource "digitalocean_spaces_bucket_cors_configuration" "test" { - bucket = digitalocean_spaces_bucket.foobar.id - region = "nyc3" + bucket = digitalocean_spaces_bucket.foobar.id + region = "nyc3" cors_rule { allowed_headers = ["*"] @@ -301,14 +301,14 @@ resource "digitalocean_spaces_bucket_cors_configuration" "test" { func testAccSpacesBucketCORSConfigurationConfig_multipleRules(rName string, region string, Name string) string { return fmt.Sprintf(` resource "digitalocean_spaces_bucket" "foobar" { - region = "%s" - name = "%s" - force_destroy = true - } + region = "%s" + name = "%s" + force_destroy = true +} resource "digitalocean_spaces_bucket_cors_configuration" "test" { - bucket = digitalocean_spaces_bucket.foobar.id - region = "nyc3" + bucket = digitalocean_spaces_bucket.foobar.id + region = "nyc3" cors_rule { allowed_headers = ["*"] From 6e1f11908a2a588df6984c224a5682da660edec9 Mon Sep 17 00:00:00 2001 From: danaelhe <42972711+danaelhe@users.noreply.github.com> Date: Wed, 23 Aug 2023 14:02:50 -0400 Subject: [PATCH 3/6] test fixes + deprecation --- digitalocean/spaces/resource_spaces_bucket.go | 1 + ...source_spaces_bucket_cors_configuration.go | 146 +++++++++++------- ...e_spaces_bucket_cors_configuration_test.go | 12 +- 3 files changed, 94 insertions(+), 65 deletions(-) diff --git a/digitalocean/spaces/resource_spaces_bucket.go b/digitalocean/spaces/resource_spaces_bucket.go index d7bd01d7e..7f856d5ee 100644 --- a/digitalocean/spaces/resource_spaces_bucket.go +++ b/digitalocean/spaces/resource_spaces_bucket.go @@ -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.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "allowed_methods": { diff --git a/digitalocean/spaces/resource_spaces_bucket_cors_configuration.go b/digitalocean/spaces/resource_spaces_bucket_cors_configuration.go index 12049126d..29dfb2520 100644 --- a/digitalocean/spaces/resource_spaces_bucket_cors_configuration.go +++ b/digitalocean/spaces/resource_spaces_bucket_cors_configuration.go @@ -36,28 +36,35 @@ func ResourceDigitalOceanBucketCorsConfiguration() *schema.Resource { ValidateFunc: validation.StringInSlice(SpacesRegions, true), }, "cors_rule": { - Type: schema.TypeList, - Optional: true, - Description: "A container holding a list of elements describing allowed methods for a specific origin.", + Type: schema.TypeSet, + Required: true, + MaxItems: 100, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "allowed_headers": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, "allowed_methods": { - Type: schema.TypeList, - Required: true, - Description: "A list of HTTP methods (e.g. GET) which are allowed from the specified origin.", - Elem: &schema.Schema{Type: schema.TypeString}, + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, "allowed_origins": { - Type: schema.TypeList, - Required: true, - Description: "A list of hosts from which requests using the specified methods are allowed. A host may contain one wildcard (e.g. http://*.example.com).", - Elem: &schema.Schema{Type: schema.TypeString}, + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, - "allowed_headers": { - Type: schema.TypeList, - Optional: true, - Description: "A list of headers that will be included in the CORS preflight request's Access-Control-Request-Headers. A header may contain one wildcard (e.g. x-amz-*).", - 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, @@ -77,16 +84,11 @@ func resourceDigitalOceanBucketCorsConfigurationCreate(ctx context.Context, d *s } bucket := d.Get("bucket").(string) - rawCors := d.Get("cors_rule").([]interface{}) - - if len(rawCors) == 0 { - return diag.Errorf("Spaces bucket CORS must not be empty") - } input := &s3.PutBucketCorsInput{ Bucket: aws.String(bucket), CORSConfiguration: &s3.CORSConfiguration{ - CORSRules: expandBucketCorsConfigurationCorsRules(rawCors, bucket), + CORSRules: expandBucketCorsConfigurationCorsRules(d.Get("cors_rule").(*schema.Set).List()), }, } @@ -101,7 +103,7 @@ func resourceDigitalOceanBucketCorsConfigurationCreate(ctx context.Context, d *s } d.SetId(bucket) - return resourceDigitalOceanBucketPolicyRead(ctx, d, meta) + return resourceDigitalOceanBucketCorsConfigurationRead(ctx, d, meta) } func resourceDigitalOceanBucketCorsConfigurationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -155,42 +157,6 @@ func resourceBucketCorsConfigurationDelete(ctx context.Context, d *schema.Resour } -func expandBucketCorsConfigurationCorsRules(rawCors []interface{}, bucketName string) []*s3.CORSRule { - if len(rawCors) == 0 { - return nil - } - - 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] Spaces bucket: %s, put CORS: %#v, %#v", bucketName, 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) - } - return rules -} - func s3connFromSpacesBucketCorsResourceData(d *schema.ResourceData, meta interface{}) (*s3.S3, error) { region := d.Get("region").(string) @@ -257,3 +223,65 @@ func flattenStringList(list []*string) []interface{} { } 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 +} diff --git a/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go b/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go index 0399ba156..a66ec8f87 100644 --- a/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go +++ b/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go @@ -258,8 +258,8 @@ func testAccCheckDigitalOceanSpacesBucketCorsConfigurationDestroy(s *terraform.S func testAccSpacesBucketCORSConfigurationConfig_basic(rName string, region string, origin string) string { return fmt.Sprintf(` resource "digitalocean_spaces_bucket" "foobar" { - name = %s - region = %s + name = "%s" + region = "%s" } resource "digitalocean_spaces_bucket_cors_configuration" "test" { @@ -277,8 +277,8 @@ resource "digitalocean_spaces_bucket_cors_configuration" "test" { func testAccSpacesBucketCORSConfigurationConfig_completeSingleRule(rName string, region string, Name string) string { return fmt.Sprintf(` resource "digitalocean_spaces_bucket" "foobar" { - region = "%s" name = "%s" + region = "%s" force_destroy = true } @@ -291,7 +291,7 @@ resource "digitalocean_spaces_bucket_cors_configuration" "test" { allowed_methods = ["PUT", "POST", "DELETE"] allowed_origins = ["https://www.example.com"] expose_headers = ["ETag"] - id = %s + id = "%s" max_age_seconds = 3000 } } @@ -301,8 +301,8 @@ resource "digitalocean_spaces_bucket_cors_configuration" "test" { func testAccSpacesBucketCORSConfigurationConfig_multipleRules(rName string, region string, Name string) string { return fmt.Sprintf(` resource "digitalocean_spaces_bucket" "foobar" { - region = "%s" name = "%s" + region = "%s" force_destroy = true } @@ -315,7 +315,7 @@ resource "digitalocean_spaces_bucket_cors_configuration" "test" { allowed_methods = ["PUT", "POST", "DELETE"] allowed_origins = ["https://www.example.com"] expose_headers = ["ETag"] - id = %s + id = "%s" max_age_seconds = 3000 } From 58012459497d30365a8364902640f342af4d1660 Mon Sep 17 00:00:00 2001 From: danaelhe <42972711+danaelhe@users.noreply.github.com> Date: Wed, 23 Aug 2023 14:19:59 -0400 Subject: [PATCH 4/6] typo --- ...source_spaces_bucket_cors_configuration.go | 2 +- examples/spaces/main.tf | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 examples/spaces/main.tf diff --git a/digitalocean/spaces/resource_spaces_bucket_cors_configuration.go b/digitalocean/spaces/resource_spaces_bucket_cors_configuration.go index 29dfb2520..90c7e63c5 100644 --- a/digitalocean/spaces/resource_spaces_bucket_cors_configuration.go +++ b/digitalocean/spaces/resource_spaces_bucket_cors_configuration.go @@ -80,7 +80,7 @@ func ResourceDigitalOceanBucketCorsConfiguration() *schema.Resource { 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) + return diag.Errorf("Error occurred while configuring CORS for Spaces bucket: %s", err) } bucket := d.Get("bucket").(string) diff --git a/examples/spaces/main.tf b/examples/spaces/main.tf new file mode 100644 index 000000000..00f3315ec --- /dev/null +++ b/examples/spaces/main.tf @@ -0,0 +1,26 @@ +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = ">= 2.4.0" + } + } +} + +resource "digitalocean_spaces_bucket" "foobar" { + name = "samiemadidriesbengals" + region = "nyc3" + } + +resource "digitalocean_spaces_bucket_cors_configuration" "foo" { + bucket = digitalocean_spaces_bucket.foobar.id + region = "nyc3" + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["PUT", "POST"] + allowed_origins = ["https://s3-website-test.hashicorp.com"] + expose_headers = ["ETag"] + max_age_seconds = 3000 + } +} \ No newline at end of file From 766c44684c326c4c86a4412f2499bb1b95913b16 Mon Sep 17 00:00:00 2001 From: danaelhe <42972711+danaelhe@users.noreply.github.com> Date: Tue, 29 Aug 2023 11:54:40 -0400 Subject: [PATCH 5/6] update test --- .../resource_spaces_bucket_cors_configuration_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go b/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go index a66ec8f87..d790af746 100644 --- a/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go +++ b/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go @@ -61,7 +61,7 @@ func TestAccS3BucketCorsConfiguration_SingleRule(t *testing.T) { Config: testAccSpacesBucketCORSConfigurationConfig_completeSingleRule(rName, region, rName), Check: resource.ComposeTestCheckFunc( testAccCheckDigitalOceanSpacesBucketCorsConfigurationExists(ctx, resourceName), - resource.TestCheckResourceAttrPair(resourceName, "bucket", "digitalocean_spaces_bucket.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "digitalocean_spaces_bucket.foobar", "id"), resource.TestCheckResourceAttr(resourceName, "cors_rule.#", "1"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "cors_rule.*", map[string]string{ "allowed_headers.#": "1", @@ -98,7 +98,7 @@ func TestAccS3BucketCorsConfiguration_MultipleRules(t *testing.T) { Config: testAccSpacesBucketCORSConfigurationConfig_multipleRules(rName, region, rName), Check: resource.ComposeTestCheckFunc( testAccCheckDigitalOceanSpacesBucketCorsConfigurationExists(ctx, resourceName), - resource.TestCheckResourceAttrPair(resourceName, "bucket", "digitalocean_spaces_bucket.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "digitalocean_spaces_bucket.foobar", "id"), resource.TestCheckResourceAttr(resourceName, "cors_rule.#", "2"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "cors_rule.*", map[string]string{ "allowed_headers.#": "1", @@ -137,7 +137,7 @@ func TestAccS3BucketCorsConfiguration_update(t *testing.T) { Config: testAccSpacesBucketCORSConfigurationConfig_completeSingleRule(rName, region, rName), Check: resource.ComposeTestCheckFunc( testAccCheckDigitalOceanSpacesBucketCorsConfigurationExists(ctx, resourceName), - resource.TestCheckResourceAttrPair(resourceName, "bucket", "digitalocean_spaces_bucket.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "digitalocean_spaces_bucket.foobar", "id"), resource.TestCheckResourceAttr(resourceName, "cors_rule.#", "1"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "cors_rule.*", map[string]string{ "allowed_headers.#": "1", @@ -153,7 +153,7 @@ func TestAccS3BucketCorsConfiguration_update(t *testing.T) { Config: testAccSpacesBucketCORSConfigurationConfig_multipleRules(rName, region, rName), Check: resource.ComposeTestCheckFunc( testAccCheckDigitalOceanSpacesBucketCorsConfigurationExists(ctx, resourceName), - resource.TestCheckResourceAttrPair(resourceName, "bucket", "digitalocean_spaces_bucket.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "digitalocean_spaces_bucket.foobar", "id"), resource.TestCheckResourceAttr(resourceName, "cors_rule.#", "2"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "cors_rule.*", map[string]string{ "allowed_headers.#": "1", From bdb752e7c0b2be287faab0e605e8995a51563413 Mon Sep 17 00:00:00 2001 From: danaelhe <42972711+danaelhe@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:37:13 -0400 Subject: [PATCH 6/6] fix spaces_bucket_test test too --- .../resource_spaces_bucket_cors_configuration_test.go | 8 ++++---- digitalocean/spaces/resource_spaces_bucket_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go b/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go index d790af746..f4349e9da 100644 --- a/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go +++ b/digitalocean/spaces/resource_spaces_bucket_cors_configuration_test.go @@ -18,7 +18,7 @@ const ( testAccDigitalOceanSpacesBucketCorsConfiguration_TestRegion = "nyc3" ) -func TestAccDigitalOceanBucketCorsConfiguration_basic(t *testing.T) { +func TestAccDigitalOceanSpacesBucketCorsConfiguration_basic(t *testing.T) { name := acceptance.RandomTestName() ctx := context.Background() region := testAccDigitalOceanSpacesBucketCorsConfiguration_TestRegion @@ -46,7 +46,7 @@ func TestAccDigitalOceanBucketCorsConfiguration_basic(t *testing.T) { }) } -func TestAccS3BucketCorsConfiguration_SingleRule(t *testing.T) { +func TestAccDigitalOceanSpacesBucketCorsConfiguration_SingleRule(t *testing.T) { resourceName := "digitalocean_spaces_bucket_cors_configuration.test" rName := acceptance.RandomTestName() ctx := context.Background() @@ -83,7 +83,7 @@ func TestAccS3BucketCorsConfiguration_SingleRule(t *testing.T) { }) } -func TestAccS3BucketCorsConfiguration_MultipleRules(t *testing.T) { +func TestAccDigitalOceanSpacesBucketCorsConfiguration_MultipleRules(t *testing.T) { resourceName := "digitalocean_spaces_bucket_cors_configuration.test" rName := acceptance.RandomTestName() ctx := context.Background() @@ -122,7 +122,7 @@ func TestAccS3BucketCorsConfiguration_MultipleRules(t *testing.T) { }) } -func TestAccS3BucketCorsConfiguration_update(t *testing.T) { +func TestAccDigitalOceanSpacesBucketCorsConfiguration_update(t *testing.T) { resourceName := "digitalocean_spaces_bucket_cors_configuration.test" rName := acceptance.RandomTestName() ctx := context.Background() diff --git a/digitalocean/spaces/resource_spaces_bucket_test.go b/digitalocean/spaces/resource_spaces_bucket_test.go index 09b0a3e63..1187942dc 100644 --- a/digitalocean/spaces/resource_spaces_bucket_test.go +++ b/digitalocean/spaces/resource_spaces_bucket_test.go @@ -111,7 +111,7 @@ func TestAccDigitalOceanBucket_UpdateCors(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckDigitalOceanBucketExists("digitalocean_spaces_bucket.bucket"), resource.TestCheckNoResourceAttr( - "digitalocean_spaces_bucket.bucket", "cors_rule"), + "digitalocean_spaces_bucket.bucket", "cors_rule.#"), ), }, {