Repository: digitalocean/terraform-provider-digitalocean
Pull request #1342: Support Spaces Key API Updated
Author: login: amahajan-do
Labels: name: feature
description: New resources
Created: 2025-03-20T09:08:43Z | Merged: 2025-03-25T17:27:32Z

Adds support for Spaces Key API based on https://github.com/digitalocean/openapi/pull/958

This PR supersedes #1309


Diff:
diff --git a/GNUmakefile b/GNUmakefile
index b7fea66e1..39e22c44c 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -35,7 +35,7 @@ goimports:
 	@find . -name '*.go' | grep -v vendor | grep -v generator-resource-id | while read f; do goimports -w "$$f"; done
 
 install-golangci-lint:
-	@go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0
+	@go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8
 
 lint: install-golangci-lint
 	@golangci-lint run -v ./...
diff --git a/digitalocean/provider.go b/digitalocean/provider.go
index 6e45e11f3..b8e7dd930 100644
--- a/digitalocean/provider.go
+++ b/digitalocean/provider.go
@@ -132,6 +132,7 @@ func Provider() *schema.Provider {
 			"digitalocean_sizes":                    size.DataSourceDigitalOceanSizes(),
 			"digitalocean_spaces_bucket":            spaces.DataSourceDigitalOceanSpacesBucket(),
 			"digitalocean_spaces_buckets":           spaces.DataSourceDigitalOceanSpacesBuckets(),
+			"digitalocean_spaces_key":               spaces.DataSourceDigitalOceanSpacesKey(),
 			"digitalocean_spaces_bucket_object":     spaces.DataSourceDigitalOceanSpacesBucketObject(),
 			"digitalocean_spaces_bucket_objects":    spaces.DataSourceDigitalOceanSpacesBucketObjects(),
 			"digitalocean_ssh_key":                  sshkey.DataSourceDigitalOceanSSHKey(),
@@ -185,6 +186,7 @@ func Provider() *schema.Provider {
 			"digitalocean_spaces_bucket_cors_configuration":      spaces.ResourceDigitalOceanBucketCorsConfiguration(),
 			"digitalocean_spaces_bucket_object":                  spaces.ResourceDigitalOceanSpacesBucketObject(),
 			"digitalocean_spaces_bucket_policy":                  spaces.ResourceDigitalOceanSpacesBucketPolicy(),
+			"digitalocean_spaces_key":                            spaces.ResourceDigitalOceanSpacesKey(),
 			"digitalocean_ssh_key":                               sshkey.ResourceDigitalOceanSSHKey(),
 			"digitalocean_tag":                                   tag.ResourceDigitalOceanTag(),
 			"digitalocean_uptime_check":                          uptime.ResourceDigitalOceanUptimeCheck(),
diff --git a/digitalocean/spaces/datasource_spaces_key.go b/digitalocean/spaces/datasource_spaces_key.go
new file mode 100644
index 000000000..17248ba63
--- /dev/null
+++ b/digitalocean/spaces/datasource_spaces_key.go
@@ -0,0 +1,41 @@
+package spaces
+
+import (
+	"context"
+	"log"
+
+	"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"
+)
+
+func DataSourceDigitalOceanSpacesKey() *schema.Resource {
+	return &schema.Resource{
+		ReadContext: dataSourceSpacesKeyRead,
+
+		Schema: spacesKeyDataSourceSchema(),
+	}
+}
+
+func dataSourceSpacesKeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+	client := meta.(*config.CombinedConfig).GodoClient()
+
+	key, _, err := client.SpacesKeys.Get(ctx, d.Get("access_key").(string))
+	if err != nil {
+		return diag.Errorf("Error reading Spaces key: %s", err)
+	}
+
+	if key == nil {
+		log.Printf("[WARN] Key not found: %s", d.Id())
+		d.SetId("")
+		return nil
+	}
+
+	d.SetId(key.AccessKey)
+	d.Set("name", key.Name)
+	d.Set("access_key", key.AccessKey)
+	d.Set("grant", flattenGrants(key.Grants))
+	d.Set("created_at", key.CreatedAt)
+	return nil
+}
diff --git a/digitalocean/spaces/datasource_spaces_key_test.go b/digitalocean/spaces/datasource_spaces_key_test.go
new file mode 100644
index 000000000..4dad1c03b
--- /dev/null
+++ b/digitalocean/spaces/datasource_spaces_key_test.go
@@ -0,0 +1,96 @@
+package spaces_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"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/helper/schema"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+)
+
+func TestAccDataSourceDigitalOceanSpacesKey_basic(t *testing.T) {
+	name := acceptance.RandomTestName()
+	resources, both := testAccDataSourceDigitalOceanSpacesKeyConfig_basic(name)
+
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:                  func() { acceptance.TestAccPreCheck(t) },
+		ProviderFactories:         acceptance.TestAccProviderFactories,
+		CheckDestroy:              testAccCheckDigitalOceanSpacesKeyDestroy,
+		PreventPostDestroyRefresh: true,
+		Steps: []resource.TestStep{
+			{
+				Config: resources,
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("digitalocean_spaces_key.key", "name", name),
+					resource.TestCheckResourceAttr("digitalocean_spaces_key.key", "grant.0.bucket", "my-bucket"),
+					resource.TestCheckResourceAttr("digitalocean_spaces_key.key", "grant.0.permission", "read"),
+				),
+			},
+			{
+				Config: both,
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("data.digitalocean_spaces_key.key", "name", name),
+					resource.TestCheckResourceAttr("data.digitalocean_spaces_key.key", "grant.0.bucket", "my-bucket"),
+					resource.TestCheckResourceAttr("data.digitalocean_spaces_key.key", "grant.0.permission", "read"),
+				),
+			},
+		},
+	})
+}
+
+func testAccDataSourceDigitalOceanSpacesKeyConfig_basic(name string) (string, string) {
+	resources := fmt.Sprintf(`
+resource "digitalocean_spaces_key" "key" {
+  name = "%s"
+  grant {
+    bucket     = "my-bucket"
+    permission = "read"
+  }
+  grant {
+    bucket     = "my-bucket2"
+    permission = "readwrite"
+  }
+}
+`, name)
+
+	both := fmt.Sprintf(`%s
+data "digitalocean_spaces_key" "key" {
+  name = "%s"
+}
+`, resources, name)
+
+	return resources, both
+}
+
+func testAccCheckDigitalOceanSpacesKeyDestroy(s *terraform.State) error {
+	return testAccCheckDigitalOceanSpacesKeyDestroyWithProvider(s, acceptance.TestAccProvider)
+}
+
+func testAccCheckDigitalOceanSpacesKeyDestroyWithProvider(s *terraform.State, provider *schema.Provider) error {
+	for _, rs := range s.RootModule().Resources {
+		if rs.Type != "digitalocean_spaces_key" {
+			continue
+		}
+
+		if rs.Primary.ID == "" {
+			return fmt.Errorf("No Record ID is not set")
+		}
+
+		client := provider.Meta().(*config.CombinedConfig).GodoClient()
+
+		key, _, err := client.SpacesKeys.Get(context.Background(), rs.Primary.ID)
+		if err != nil {
+			return fmt.Errorf("Error listing Spaces keys: %s", err)
+		}
+		if key.AccessKey == rs.Primary.ID {
+			return fmt.Errorf("Key still exists")
+		}
+		return nil
+	}
+	return nil
+}
diff --git a/digitalocean/spaces/resource_spaces_bucket.go b/digitalocean/spaces/resource_spaces_bucket.go
index f83362815..28cd704cf 100644
--- a/digitalocean/spaces/resource_spaces_bucket.go
+++ b/digitalocean/spaces/resource_spaces_bucket.go
@@ -8,11 +8,12 @@ import (
 	"strings"
 	"time"
 
+	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config"
+	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/util"
+
 	"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/digitalocean/terraform-provider-digitalocean/digitalocean/util"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/id"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
diff --git a/digitalocean/spaces/resource_spaces_key.go b/digitalocean/spaces/resource_spaces_key.go
new file mode 100644
index 000000000..3c282795e
--- /dev/null
+++ b/digitalocean/spaces/resource_spaces_key.go
@@ -0,0 +1,158 @@
+package spaces
+
+import (
+	"context"
+	"log"
+
+	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config"
+
+	"github.com/digitalocean/godo"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+)
+
+func ResourceDigitalOceanSpacesKey() *schema.Resource {
+	return &schema.Resource{
+		CreateContext: resourceDigitalOceanSpacesKeyCreate,
+		ReadContext:   resourceDigitalOceanSpacesKeyRead,
+		UpdateContext: resourceDigitalOceanSpacesKeyUpdate,
+		DeleteContext: resourceDigitalOceanSpacesKeyDelete,
+
+		Schema: spacesKeyResourceSchema(),
+	}
+}
+
+func resourceDigitalOceanSpacesKeyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+	client := meta.(*config.CombinedConfig).GodoClient()
+
+	name := d.Get("name").(string)
+	rawGrants := d.Get("grant").([]interface{})
+
+	req := &godo.SpacesKeyCreateRequest{
+		Name:   name,
+		Grants: parseRawGrants(rawGrants),
+	}
+
+	var key *godo.SpacesKey
+	var err error
+	log.Printf("[DEBUG] Creating new Spaces key")
+	key, _, err = client.SpacesKeys.Create(ctx, req)
+	if err != nil {
+		return diag.Errorf("Error creating Spaces key: %s", err)
+	}
+
+	log.Println("Spaces Key created")
+	d.SetId(key.AccessKey)
+	d.Set("name", key.Name)
+	d.Set("access_key", key.AccessKey)
+	d.Set("secret_key", key.SecretKey)
+	d.Set("grant", flattenGrants(key.Grants))
+	d.Set("created_at", key.CreatedAt)
+	return nil
+}
+
+func resourceDigitalOceanSpacesKeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+	client := meta.(*config.CombinedConfig).GodoClient()
+
+	var key *godo.SpacesKey
+
+	key, _, err := client.SpacesKeys.Get(ctx, d.Id())
+	if err != nil {
+		return diag.Errorf("Error reading Spaces key: %s", err)
+	}
+
+	if key == nil {
+		log.Printf("[WARN] Key not found: %s", d.Id())
+		d.SetId("")
+		return nil
+	}
+
+	d.Set("name", key.Name)
+	d.Set("access_key", key.AccessKey)
+	d.Set("grant", flattenGrants(key.Grants))
+	d.Set("created_at", key.CreatedAt)
+	return nil
+}
+
+func resourceDigitalOceanSpacesKeyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+	client := meta.(*config.CombinedConfig).GodoClient()
+
+	name := d.Get("name").(string)
+	rawGrants := d.Get("grant").([]interface{})
+
+	req := &godo.SpacesKeyUpdateRequest{
+		Name:   name,
+		Grants: parseRawGrants(rawGrants),
+	}
+
+	var key *godo.SpacesKey
+	var err error
+	log.Printf("[DEBUG] Updating Spaces key: %s", name)
+	key, _, err = client.SpacesKeys.Update(ctx, d.Id(), req)
+	if err != nil {
+		return diag.Errorf("Error updating Spaces key: %s", err)
+	}
+
+	log.Println("Spaces Key updated")
+	d.Set("name", key.Name)
+	d.Set("access_key", key.AccessKey)
+	d.Set("grant", flattenGrants(key.Grants))
+	d.Set("created_at", key.CreatedAt)
+	return nil
+}
+
+func resourceDigitalOceanSpacesKeyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+	client := meta.(*config.CombinedConfig).GodoClient()
+
+	log.Printf("[DEBUG] Deleting Spaces key: %s", d.Id())
+	_, err := client.SpacesKeys.Delete(ctx, d.Id())
+	if err != nil {
+		return diag.Errorf("Error deleting Spaces key: %s", err)
+	}
+
+	log.Println("Spaces Key deleted")
+	d.SetId("")
+	return nil
+}
+
+func parseRawGrants(rawGrants []interface{}) []*godo.Grant {
+	grants := make([]*godo.Grant, 0, len(rawGrants))
+	for _, rawGrant := range rawGrants {
+		grant := rawGrant.(map[string]interface{})
+		g := &godo.Grant{}
+		for k, v := range grant {
+			if k == "bucket" {
+				g.Bucket = v.(string)
+			} else {
+				switch v.(string) {
+				case "read":
+					g.Permission = godo.SpacesKeyRead
+				case "readwrite":
+					g.Permission = godo.SpacesKeyReadWrite
+				case "fullaccess":
+					g.Permission = godo.SpacesKeyFullAccess
+				}
+			}
+		}
+		grants = append(grants, g)
+	}
+	return grants
+}
+
+func flattenGrants(grants []*godo.Grant) []map[string]interface{} {
+	results := make([]map[string]interface{}, 0, len(grants))
+	for _, grant := range grants {
+		g := make(map[string]interface{})
+		g["bucket"] = grant.Bucket
+		switch grant.Permission {
+		case godo.SpacesKeyRead:
+			g["permission"] = "read"
+		case godo.SpacesKeyReadWrite:
+			g["permission"] = "readwrite"
+		case godo.SpacesKeyFullAccess:
+			g["permission"] = "fullaccess"
+		}
+		results = append(results, g)
+	}
+	return results
+}
diff --git a/digitalocean/spaces/resource_spaces_key_test.go b/digitalocean/spaces/resource_spaces_key_test.go
new file mode 100644
index 000000000..714c90d3b
--- /dev/null
+++ b/digitalocean/spaces/resource_spaces_key_test.go
@@ -0,0 +1,175 @@
+package spaces_test
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/acceptance"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+)
+
+func TestAccDigitalOceanSpacesKey_basic(t *testing.T) {
+	expectedName := acceptance.RandomTestName()
+	bucketName := acceptance.RandomTestName()
+
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:          func() { acceptance.TestAccPreCheck(t) },
+		ProviderFactories: acceptance.TestAccProviderFactories,
+		CheckDestroy:      testAccCheckDigitalOceanSpacesKeyDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccDigitalOceanSpacesKeyConfig(expectedName, bucketName),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "name", expectedName),
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "grant.0.bucket", bucketName),
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "grant.0.permission", "read"),
+				),
+			},
+		},
+	})
+}
+
+func TestAccDigitalOceanSpacesKey_updateGrant(t *testing.T) {
+	expectedName := acceptance.RandomTestName()
+	expectedNewName := acceptance.RandomTestName()
+	bucketName := acceptance.RandomTestName()
+
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:          func() { acceptance.TestAccPreCheck(t) },
+		ProviderFactories: acceptance.TestAccProviderFactories,
+		CheckDestroy:      testAccCheckDigitalOceanSpacesKeyDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccDigitalOceanSpacesKeyConfig(expectedName, bucketName),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "name", expectedName),
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "grant.0.bucket", bucketName),
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "grant.0.permission", "read"),
+				),
+			},
+			{
+				Config: testAccDigitalOceanSpacesKeyConfigWithGrantUpdate(bucketName, expectedNewName, bucketName),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "name", expectedNewName),
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "grant.0.bucket", bucketName),
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "grant.0.permission", "read"),
+				),
+			},
+		},
+	})
+}
+
+func TestAccDigitalOceanSpacesKey_multipleGrants(t *testing.T) {
+	expectedName := acceptance.RandomTestName()
+	bucketName := acceptance.RandomTestName()
+	bucketName2 := acceptance.RandomTestName()
+
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:          func() { acceptance.TestAccPreCheck(t) },
+		ProviderFactories: acceptance.TestAccProviderFactories,
+		CheckDestroy:      testAccCheckDigitalOceanSpacesKeyDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccDigitalOceanSpacesKeyConfigMultipleGrants(expectedName, bucketName, bucketName2),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "name", expectedName),
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "grant.0.bucket", bucketName),
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "grant.0.permission", "read"),
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "grant.1.bucket", bucketName2),
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "grant.1.permission", "readwrite"),
+				),
+			},
+			{
+				Config: testAccDigitalOceanSpacesKeyConfig(expectedName, bucketName),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "name", expectedName),
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "grant.0.bucket", bucketName),
+					resource.TestCheckResourceAttr(
+						"digitalocean_spaces_key.key", "grant.0.permission", "read"),
+					resource.TestCheckNoResourceAttr(
+						"digitalocean_spaces_key.key", "grant.1.bucket"),
+					resource.TestCheckNoResourceAttr(
+						"digitalocean_spaces_key.key", "grant.1.permission"),
+				),
+			},
+		},
+	})
+}
+
+func testAccDigitalOceanSpacesKeyConfig(name, bucket string) string {
+	return fmt.Sprintf(`
+resource "digitalocean_spaces_bucket" "bucket" {
+  name   = "%s"
+  acl    = "public-read"
+  region = "sfo3"
+}
+resource "digitalocean_spaces_key" "key" {
+  name = "%s"
+  grant {
+    bucket     = digitalocean_spaces_bucket.bucket.name
+    permission = "read"
+  }
+}
+`, bucket, name)
+}
+
+func testAccDigitalOceanSpacesKeyConfigWithGrantUpdate(bucketName, spacesKeyName, bucketName2 string) string {
+	return fmt.Sprintf(`
+resource "digitalocean_spaces_bucket" "bucket" {
+  name   = "%s"
+  acl    = "public-read"
+  region = "sfo3"
+}
+
+resource "digitalocean_spaces_key" "key" {
+  name = "%s"
+  grant {
+    bucket     = "%s"
+    permission = "read"
+  }
+}
+`, bucketName, spacesKeyName, bucketName2)
+}
+
+func testAccDigitalOceanSpacesKeyConfigMultipleGrants(name, bucket, bucket2 string) string {
+	return fmt.Sprintf(`
+resource "digitalocean_spaces_bucket" "bucket" {
+  name   = "%s"
+  acl    = "public-read"
+  region = "sfo3"
+}
+resource "digitalocean_spaces_bucket" "bucket2" {
+  name   = "%s"
+  acl    = "public-read"
+  region = "sfo3"
+}
+resource "digitalocean_spaces_key" "key" {
+  name = "%s"
+  grant {
+    bucket     = digitalocean_spaces_bucket.bucket.name
+    permission = "read"
+  }
+  grant {
+    bucket     = digitalocean_spaces_bucket.bucket2.name
+    permission = "readwrite"
+  }
+}
+`, bucket, bucket2, name)
+}
diff --git a/digitalocean/spaces/spaces_buckets.go b/digitalocean/spaces/spaces_entities.go
similarity index 62%
rename from digitalocean/spaces/spaces_buckets.go
rename to digitalocean/spaces/spaces_entities.go
index b0f890203..cdac58c18 100644
--- a/digitalocean/spaces/spaces_buckets.go
+++ b/digitalocean/spaces/spaces_entities.go
@@ -3,9 +3,10 @@ package spaces
 import (
 	"fmt"
 
+	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config"
+
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/s3"
-	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config"
 	awspolicy "github.com/hashicorp/awspolicyequivalence"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 )
@@ -45,6 +46,90 @@ func spacesBucketSchema() map[string]*schema.Schema {
 	}
 }
 
+func spacesKeyDataSourceSchema() map[string]*schema.Schema {
+	return map[string]*schema.Schema{
+		"name": {
+			Type:        schema.TypeString,
+			Description: "A name for the key. This is used to identify the key in the DigitalOcean control panel.",
+			Computed:    true,
+		},
+		"grant": {
+			Type:        schema.TypeList,
+			Description: "A list of grants to apply to the key. Can be left empty to apply no grants.",
+			Computed:    true,
+			Elem: &schema.Resource{
+				Schema: map[string]*schema.Schema{
+					"bucket": {
+						Type:        schema.TypeString,
+						Required:    true,
+						Description: "The name of the bucket to grant the key access to.",
+					},
+					"permission": {
+						Type:        schema.TypeString,
+						Required:    true,
+						Description: "The permission to grant the key. Valid values are `read`, `readwrite`, or `fullaccess`.",
+					},
+				},
+			},
+		},
+		"access_key": {
+			Type:        schema.TypeString,
+			Description: "The access key for the Spaces key",
+			Required:    true,
+		},
+		"created_at": {
+			Type:        schema.TypeString,
+			Description: "The date and time the key was created",
+			Computed:    true,
+		},
+	}
+}
+
+func spacesKeyResourceSchema() map[string]*schema.Schema {
+	return map[string]*schema.Schema{
+		"name": {
+			Type:        schema.TypeString,
+			Required:    true,
+			Description: "A name for the key. This is used to identify the key in the DigitalOcean control panel.",
+		},
+		"grant": {
+			Type:        schema.TypeList,
+			Optional:    true,
+			Description: "A list of grants to apply to the key. Can be left empty to apply no grants.",
+			Elem: &schema.Resource{
+				Schema: map[string]*schema.Schema{
+					"bucket": {
+						Type:        schema.TypeString,
+						Required:    true,
+						Description: "The name of the bucket to grant the key access to.",
+					},
+					"permission": {
+						Type:        schema.TypeString,
+						Required:    true,
+						Description: "The permission to grant the key. Valid values are `read`, `readwrite`, or `fullaccess`.",
+					},
+				},
+			},
+		},
+		"access_key": {
+			Type:        schema.TypeString,
+			Description: "The access key for the Spaces key",
+			Computed:    true,
+		},
+		"secret_key": {
+			Type:        schema.TypeString,
+			Description: "The secret key for the Spaces key",
+			Computed:    true,
+			Sensitive:   true,
+		},
+		"created_at": {
+			Type:        schema.TypeString,
+			Description: "The date and time the key was created",
+			Computed:    true,
+		},
+	}
+}
+
 func getSpacesBucketsInRegion(meta interface{}, region string) ([]*s3.Bucket, error) {
 	client, err := meta.(*config.CombinedConfig).SpacesClient(region)
 	if err != nil {
diff --git a/docs/data-sources/spaces_key.md b/docs/data-sources/spaces_key.md
new file mode 100644
index 000000000..44fe5b5ac
--- /dev/null
+++ b/docs/data-sources/spaces_key.md
@@ -0,0 +1,38 @@
+---
+page_title: "DigitalOcean: digitalocean_spaces_key"
+subcategory: "Spaces Object Storage"
+---
+
+# digitalocean_spaces_key
+
+Get information on a Spaces key for use in other resources. This is useful if the Spaces key in question
+is not managed by Terraform or you need to utilize any of the key's data.
+
+## Example Usage
+
+Get the key by access key ID:
+
+```hcl
+data "digitalocean_spaces_key" "example" {
+  access_key = "ACCESS_KEY_ID"
+}
+
+output "key_grants" {
+  value = data.digitalocean_spaces_key.example.grant
+}
+```
+
+## Argument Reference
+
+The following arguments must be provided:
+
+* `access_key` - (Required) The Access Key ID of the Spaces key.
+
+## Attributes Reference
+
+The following attributes are exported:
+
+* `name` - The name of the Spaces key
+* `grant` - The list of grants associated with the Spaces key.
+* `access_key` - The access key ID of the Spaces key
+* `created_at` - The creation time of the Spaces key
diff --git a/docs/resources/spaces_key.md b/docs/resources/spaces_key.md
new file mode 100644
index 000000000..b671f84f7
--- /dev/null
+++ b/docs/resources/spaces_key.md
@@ -0,0 +1,76 @@
+---
+page_title: "DigitalOcean: digitalocean_spaces_key"
+subcategory: "Spaces Object Storage"
+---
+
+# digitalocean\_spaces\_key
+
+Provides a key resource for Spaces, DigitalOcean's object storage product.
+
+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.
+
+As a Spaces owner, you limit others’ access to your buckets using Spaces access 
+keys. Access keys can provide several levels of permissions to create, destroy,
+read, and write to specific associated buckets. However, access keys only limit 
+access to certain commands using the S3 API or CLI, not the control panel or 
+other DigitalOcean resources.
+
+## Example Usage
+
+### Create a New Key
+
+```hcl
+resource "digitalocean_spaces_key" "foobar" {
+  name = "foobar"
+}
+```
+
+### Create a New Key with Grants
+
+```hcl
+resource "digitalocean_spaces_key" "foobar" {
+  name = "foobar"
+  grant {
+    bucket     = "my-bucket"
+    permission = "read"
+  }
+}
+```
+
+### Create a New Key with full access
+
+```hcl
+resource "digitalocean_spaces_key" "foobar" {
+  name = "foobar"
+  grant {
+    bucket     = ""
+    permission = "fullaccess"
+  }
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `name` - (Required) The name of the key
+* `grant` - (Optional) A grant for the key (documented below).
+
+The `grant` object supports the following:
+
+* `bucket` - (Required) Name of the bucket associated with this grant. In case of a `fullaccess` permission, this value should be an empty string.
+* `permission` - (Required) Permission associated with this grant. Values can be `read`, `readwrite`, `fullaccess`.
+
+## Attributes Reference
+
+The following attributes are exported:
+
+* `name` - The name of the key
+* `grant` - The list of grants associated with the key
+* `access_key` - The access key ID of the key
+* `secret_key` - The access key secret of the key
+* `created_at` - The creation time of the key
diff --git a/go.mod b/go.mod
index 8a22fc679..79229ef51 100644
--- a/go.mod
+++ b/go.mod
@@ -2,7 +2,7 @@ module github.com/digitalocean/terraform-provider-digitalocean
 
 require (
 	github.com/aws/aws-sdk-go v1.42.18
-	github.com/digitalocean/godo v1.138.0
+	github.com/digitalocean/godo v1.140.0
 	github.com/hashicorp/awspolicyequivalence v1.5.0
 	github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
 	github.com/hashicorp/go-uuid v1.0.3
@@ -11,7 +11,7 @@ require (
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/mitchellh/hashstructure/v2 v2.0.1
 	github.com/stretchr/testify v1.8.4
-	golang.org/x/oauth2 v0.25.0
+	golang.org/x/oauth2 v0.28.0
 	gopkg.in/yaml.v2 v2.4.0
 )
 
@@ -60,7 +60,7 @@ require (
 	golang.org/x/net v0.33.0 // indirect
 	golang.org/x/sys v0.28.0 // indirect
 	golang.org/x/text v0.21.0 // indirect
-	golang.org/x/time v0.9.0 // indirect
+	golang.org/x/time v0.11.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
 	google.golang.org/grpc v1.56.3 // indirect
@@ -70,6 +70,6 @@ require (
 
 replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
 
-go 1.22
+go 1.23.0
 
-toolchain go1.22.5
+toolchain go1.23.1
diff --git a/go.sum b/go.sum
index 879e14d28..512ec0c4c 100644
--- a/go.sum
+++ b/go.sum
@@ -25,8 +25,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/digitalocean/godo v1.138.0 h1:0l0UEaVyNKvEmrn4r5NkZCpDysimJjVhLj30Gl6HgiQ=
-github.com/digitalocean/godo v1.138.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc=
+github.com/digitalocean/godo v1.140.0 h1:UM/mKsl1twOkV9hSFLT1JnQRn26U7aD+kPM98FVp7kg=
+github.com/digitalocean/godo v1.140.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc=
 github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
 github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -238,8 +238,8 @@ golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
 golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
 golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
-golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
+golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -281,8 +281,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
 golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
-golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md
index 6d6892263..5afbcba4d 100644
--- a/vendor/github.com/digitalocean/godo/CHANGELOG.md
+++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md
@@ -1,5 +1,18 @@
 # Change Log
 
+## [v1.140.0] - 2025-03-14
+
+- #800 - @lee-aaron - support Spaces Keys GET by Access Key ID
+
+## [v1.139.0] - 2025-03-12
+
+- #798 - @dylanrhysscott - Fix: Update godo to use simplified template response and provide consistent struct naming
+- #796 - @apinonformoso - fix partner interconnect attachment json request response
+- #795 - @dylanrhysscott - CON-11904 Ensure taints are correctly returned via node template endpoint
+- #794 - @brunograsselli - Update partner interconnect attachment comments
+- #793 - @apinonformoso - add auth_key field
+- #789 - @guptado - [VPC-3917] Update get service key response model
+
 ## [v1.138.0] - 2025-03-18
 
 - #785 - @guptado - Support partner interconnect GetBgpAuthKey and RegenerateServiceKey operations
diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go
index db841d5ca..829478a83 100644
--- a/vendor/github.com/digitalocean/godo/godo.go
+++ b/vendor/github.com/digitalocean/godo/godo.go
@@ -21,7 +21,7 @@ import (
 )
 
 const (
-	libraryVersion = "1.138.0"
+	libraryVersion = "1.140.0"
 	defaultBaseURL = "https://api.digitalocean.com/"
 	userAgent      = "godo/" + libraryVersion
 	mediaType      = "application/json"
diff --git a/vendor/github.com/digitalocean/godo/kubernetes.go b/vendor/github.com/digitalocean/godo/kubernetes.go
index 4a1530aa2..044b06a24 100644
--- a/vendor/github.com/digitalocean/godo/kubernetes.go
+++ b/vendor/github.com/digitalocean/godo/kubernetes.go
@@ -40,7 +40,7 @@ type KubernetesService interface {
 
 	CreateNodePool(ctx context.Context, clusterID string, req *KubernetesNodePoolCreateRequest) (*KubernetesNodePool, *Response, error)
 	GetNodePool(ctx context.Context, clusterID, poolID string) (*KubernetesNodePool, *Response, error)
-	GetNodePoolTemplate(ctx context.Context, clusterID string, nodePoolName string) (*KubernetesNodePoolTemplateResponse, *Response, error)
+	GetNodePoolTemplate(ctx context.Context, clusterID string, nodePoolName string) (*KubernetesNodePoolTemplate, *Response, error)
 	ListNodePools(ctx context.Context, clusterID string, opts *ListOptions) ([]*KubernetesNodePool, *Response, error)
 	UpdateNodePool(ctx context.Context, clusterID, poolID string, req *KubernetesNodePoolUpdateRequest) (*KubernetesNodePool, *Response, error)
 	// RecycleNodePoolNodes is DEPRECATED please use DeleteNode
@@ -435,19 +435,9 @@ type KubernetesNodePool struct {
 	Nodes []*KubernetesNode `json:"nodes,omitempty"`
 }
 
-// KubernetesNodePool represents a node pool template response from the node template endpoint
-type KubernetesNodePoolTemplateResponse struct {
-	ClusterUUID string                      `json:"cluster_uuid,omitempty"`
-	Name        string                      `json:"name,omitempty"`
-	Slug        string                      `json:"slug,omitempty"`
-	Template    *KubernetesNodePoolTemplate `json:"template,omitempty"`
-}
-
 // KubernetesNodePool represents the node pool template data for a given pool.
 type KubernetesNodePoolTemplate struct {
-	Labels      map[string]string            `json:"labels,omitempty"`
-	Capacity    *KubernetesNodePoolResources `json:"capacity,omitempty"`
-	Allocatable *KubernetesNodePoolResources `json:"allocatable,omitempty"`
+	Template *KubernetesNodeTemplate
 }
 
 // KubernetesNodePoolResources represents the resources within a given template for a node pool
@@ -470,6 +460,17 @@ type KubernetesNode struct {
 	UpdatedAt time.Time `json:"updated_at,omitempty"`
 }
 
+// KubernetesNodeTemplate represents a template in a node pool in a Kubernetes cluster.
+type KubernetesNodeTemplate struct {
+	ClusterUUID string                       `json:"cluster_uuid,omitempty"`
+	Name        string                       `json:"name,omitempty"`
+	Slug        string                       `json:"slug,omitempty"`
+	Labels      map[string]string            `json:"labels,omitempty"`
+	Taints      []string                     `json:"taints,omitempty"`
+	Capacity    *KubernetesNodePoolResources `json:"capacity,omitempty"`
+	Allocatable *KubernetesNodePoolResources `json:"allocatable,omitempty"`
+}
+
 // KubernetesNodeStatus represents the status of a particular Node in a Kubernetes cluster.
 type KubernetesNodeStatus struct {
 	State   string `json:"state,omitempty"`
@@ -838,7 +839,7 @@ func (svc *KubernetesServiceOp) GetNodePool(ctx context.Context, clusterID, pool
 }
 
 // GetNodePoolTemplate retrieves the template used for a given node pool to scale up from zero.
-func (svc *KubernetesServiceOp) GetNodePoolTemplate(ctx context.Context, clusterID string, nodePoolName string) (*KubernetesNodePoolTemplateResponse, *Response, error) {
+func (svc *KubernetesServiceOp) GetNodePoolTemplate(ctx context.Context, clusterID string, nodePoolName string) (*KubernetesNodePoolTemplate, *Response, error) {
 	path, err := url.JoinPath(kubernetesClustersPath, clusterID, "node_pools_template", nodePoolName)
 	if err != nil {
 		return nil, nil, err
@@ -847,7 +848,7 @@ func (svc *KubernetesServiceOp) GetNodePoolTemplate(ctx context.Context, cluster
 	if err != nil {
 		return nil, nil, err
 	}
-	root := new(KubernetesNodePoolTemplateResponse)
+	root := new(KubernetesNodePoolTemplate)
 	resp, err := svc.client.Do(ctx, req, root)
 	if err != nil {
 		return nil, resp, err
diff --git a/vendor/github.com/digitalocean/godo/partner_interconnect_attachments.go b/vendor/github.com/digitalocean/godo/partner_interconnect_attachments.go
index e2707dca8..dcb8ab667 100644
--- a/vendor/github.com/digitalocean/godo/partner_interconnect_attachments.go
+++ b/vendor/github.com/digitalocean/godo/partner_interconnect_attachments.go
@@ -2,6 +2,7 @@ package godo
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"time"
@@ -60,7 +61,7 @@ type partnerInterconnectAttachmentRequestBody struct {
 	// VPCIDs is the IDs of the VPCs to which the Partner Interconnect Attachment is connected
 	VPCIDs []string `json:"vpc_ids,omitempty"`
 	// BGP is the BGP configuration of the Partner Interconnect Attachment
-	BGP *BGP `json:"bgp,omitempty"`
+	BGP *BGPInput `json:"bgp,omitempty"`
 }
 
 func (req *PartnerInterconnectAttachmentCreateRequest) buildReq() *partnerInterconnectAttachmentRequestBody {
@@ -73,7 +74,13 @@ func (req *PartnerInterconnectAttachmentCreateRequest) buildReq() *partnerInterc
 	}
 
 	if req.BGP != (BGP{}) {
-		request.BGP = &req.BGP
+		request.BGP = &BGPInput{
+			LocalASN:      req.BGP.LocalASN,
+			LocalRouterIP: req.BGP.LocalRouterIP,
+			PeerASN:       req.BGP.PeerASN,
+			PeerRouterIP:  req.BGP.PeerRouterIP,
+			AuthKey:       req.BGP.AuthKey,
+		}
 	}
 
 	return request
@@ -99,15 +106,61 @@ type BGP struct {
 	// LocalRouterIP is the local router IP
 	LocalRouterIP string `json:"local_router_ip,omitempty"`
 	// PeerASN is the peer ASN
+	PeerASN int `json:"peer_asn,omitempty"`
+	// PeerRouterIP is the peer router IP
+	PeerRouterIP string `json:"peer_router_ip,omitempty"`
+	// AuthKey is the authentication key
+	AuthKey string `json:"auth_key,omitempty"`
+}
+
+func (b *BGP) UnmarshalJSON(data []byte) error {
+	type Alias BGP
+	aux := &struct {
+		LocalASN       *int `json:"local_asn,omitempty"`
+		LocalRouterASN *int `json:"local_router_asn,omitempty"`
+		PeerASN        *int `json:"peer_asn,omitempty"`
+		PeerRouterASN  *int `json:"peer_router_asn,omitempty"`
+		*Alias
+	}{
+		Alias: (*Alias)(b),
+	}
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+
+	if aux.LocalASN != nil {
+		b.LocalASN = *aux.LocalASN
+	} else if aux.LocalRouterASN != nil {
+		b.LocalASN = *aux.LocalRouterASN
+	}
+
+	if aux.PeerASN != nil {
+		b.PeerASN = *aux.PeerASN
+	} else if aux.PeerRouterASN != nil {
+		b.PeerASN = *aux.PeerRouterASN
+	}
+	return nil
+}
+
+// BGPInput represents the BGP configuration of a Partner Interconnect Attachment.
+type BGPInput struct {
+	// LocalASN is the local ASN
+	LocalASN int `json:"local_router_asn,omitempty"`
+	// LocalRouterIP is the local router IP
+	LocalRouterIP string `json:"local_router_ip,omitempty"`
+	// PeerASN is the peer ASN
 	PeerASN int `json:"peer_router_asn,omitempty"`
 	// PeerRouterIP is the peer router IP
 	PeerRouterIP string `json:"peer_router_ip,omitempty"`
+	// AuthKey is the authentication key
+	AuthKey string `json:"auth_key,omitempty"`
 }
 
 // ServiceKey represents the service key of a Partner Interconnect Attachment.
 type ServiceKey struct {
-	ServiceKey string `json:"service_key,omitempty"`
-	State      string `json:"state,omitempty"`
+	Value     string    `json:"value,omitempty"`
+	State     string    `json:"state,omitempty"`
+	CreatedAt time.Time `json:"created_at,omitempty"`
 }
 
 // RemoteRoute represents a route for a Partner Interconnect Attachment.
@@ -284,7 +337,7 @@ func (s *PartnerInterconnectAttachmentsServiceOp) GetServiceKey(ctx context.Cont
 	return root.ServiceKey, resp, nil
 }
 
-// ListRoutes lists all routes for a Partner Interconnect Attachment.
+// ListRoutes lists all remote routes for a Partner Interconnect Attachment.
 func (s *PartnerInterconnectAttachmentsServiceOp) ListRoutes(ctx context.Context, id string, opt *ListOptions) ([]*RemoteRoute, *Response, error) {
 	path, err := addOptions(fmt.Sprintf("%s/%s/remote_routes", partnerInterconnectAttachmentsBasePath, id), opt)
 	if err != nil {
@@ -310,7 +363,7 @@ func (s *PartnerInterconnectAttachmentsServiceOp) ListRoutes(ctx context.Context
 	return root.RemoteRoutes, resp, nil
 }
 
-// SetRoutes  updates specific properties of a Partner Interconnect Attachment.
+// SetRoutes updates specific properties of a Partner Interconnect Attachment.
 func (s *PartnerInterconnectAttachmentsServiceOp) SetRoutes(ctx context.Context, id string, set *PartnerInterconnectAttachmentSetRoutesRequest) (*PartnerInterconnectAttachment, *Response, error) {
 	path := fmt.Sprintf("%s/%s/remote_routes", partnerInterconnectAttachmentsBasePath, id)
 	req, err := s.client.NewRequest(ctx, http.MethodPut, path, set)
diff --git a/vendor/github.com/digitalocean/godo/spaces_keys.go b/vendor/github.com/digitalocean/godo/spaces_keys.go
index 371d8a142..8aee31dbb 100644
--- a/vendor/github.com/digitalocean/godo/spaces_keys.go
+++ b/vendor/github.com/digitalocean/godo/spaces_keys.go
@@ -14,6 +14,7 @@ type SpacesKeysService interface {
 	Update(context.Context, string, *SpacesKeyUpdateRequest) (*SpacesKey, *Response, error)
 	Create(context.Context, *SpacesKeyCreateRequest) (*SpacesKey, *Response, error)
 	Delete(context.Context, string) (*Response, error)
+	Get(context.Context, string) (*SpacesKey, *Response, error)
 }
 
 // SpacesKeysServiceOp handles communication with the Spaces key related methods of the
@@ -163,3 +164,23 @@ func (s *SpacesKeysServiceOp) List(ctx context.Context, opts *ListOptions) ([]*S
 
 	return root.Keys, resp, nil
 }
+
+// Get retrieves a Spaces key.
+func (s *SpacesKeysServiceOp) Get(ctx context.Context, accessKey string) (*SpacesKey, *Response, error) {
+	if accessKey == "" {
+		return nil, nil, NewArgError("accessKey", "cannot be empty")
+	}
+
+	path := fmt.Sprintf("%s/%s", spacesKeysBasePath, accessKey)
+	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+	root := new(spacesKeyRoot)
+	resp, err := s.client.Do(ctx, req, root)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return root.Key, resp, nil
+}
diff --git a/vendor/golang.org/x/oauth2/oauth2.go b/vendor/golang.org/x/oauth2/oauth2.go
index 74f052aa9..eacdd7fd9 100644
--- a/vendor/golang.org/x/oauth2/oauth2.go
+++ b/vendor/golang.org/x/oauth2/oauth2.go
@@ -288,7 +288,7 @@ func (tf *tokenRefresher) Token() (*Token, error) {
 	if tf.refreshToken != tk.RefreshToken {
 		tf.refreshToken = tk.RefreshToken
 	}
-	return tk, err
+	return tk, nil
 }
 
 // reuseTokenSource is a TokenSource that holds a single token in memory
@@ -356,11 +356,15 @@ func NewClient(ctx context.Context, src TokenSource) *http.Client {
 	if src == nil {
 		return internal.ContextClient(ctx)
 	}
+	cc := internal.ContextClient(ctx)
 	return &http.Client{
 		Transport: &Transport{
-			Base:   internal.ContextClient(ctx).Transport,
+			Base:   cc.Transport,
 			Source: ReuseTokenSource(nil, src),
 		},
+		CheckRedirect: cc.CheckRedirect,
+		Jar:           cc.Jar,
+		Timeout:       cc.Timeout,
 	}
 }
 
diff --git a/vendor/golang.org/x/oauth2/pkce.go b/vendor/golang.org/x/oauth2/pkce.go
index 50593b6df..6a95da975 100644
--- a/vendor/golang.org/x/oauth2/pkce.go
+++ b/vendor/golang.org/x/oauth2/pkce.go
@@ -21,7 +21,7 @@ const (
 //
 // A fresh verifier should be generated for each authorization.
 // S256ChallengeOption(verifier) should then be passed to Config.AuthCodeURL
-// (or Config.DeviceAccess) and VerifierOption(verifier) to Config.Exchange
+// (or Config.DeviceAuth) and VerifierOption(verifier) to Config.Exchange
 // (or Config.DeviceAccessToken).
 func GenerateVerifier() string {
 	// "RECOMMENDED that the output of a suitable random number generator be
@@ -51,7 +51,7 @@ func S256ChallengeFromVerifier(verifier string) string {
 }
 
 // S256ChallengeOption derives a PKCE code challenge derived from verifier with
-// method S256. It should be passed to Config.AuthCodeURL or Config.DeviceAccess
+// method S256. It should be passed to Config.AuthCodeURL or Config.DeviceAuth
 // only.
 func S256ChallengeOption(verifier string) AuthCodeOption {
 	return challengeOption{
diff --git a/vendor/golang.org/x/time/rate/rate.go b/vendor/golang.org/x/time/rate/rate.go
index 93a798ab6..794b2e32b 100644
--- a/vendor/golang.org/x/time/rate/rate.go
+++ b/vendor/golang.org/x/time/rate/rate.go
@@ -85,7 +85,7 @@ func (lim *Limiter) Burst() int {
 // TokensAt returns the number of tokens available at time t.
 func (lim *Limiter) TokensAt(t time.Time) float64 {
 	lim.mu.Lock()
-	_, tokens := lim.advance(t) // does not mutate lim
+	tokens := lim.advance(t) // does not mutate lim
 	lim.mu.Unlock()
 	return tokens
 }
@@ -186,7 +186,7 @@ func (r *Reservation) CancelAt(t time.Time) {
 		return
 	}
 	// advance time to now
-	t, tokens := r.lim.advance(t)
+	tokens := r.lim.advance(t)
 	// calculate new number of tokens
 	tokens += restoreTokens
 	if burst := float64(r.lim.burst); tokens > burst {
@@ -307,7 +307,7 @@ func (lim *Limiter) SetLimitAt(t time.Time, newLimit Limit) {
 	lim.mu.Lock()
 	defer lim.mu.Unlock()
 
-	t, tokens := lim.advance(t)
+	tokens := lim.advance(t)
 
 	lim.last = t
 	lim.tokens = tokens
@@ -324,7 +324,7 @@ func (lim *Limiter) SetBurstAt(t time.Time, newBurst int) {
 	lim.mu.Lock()
 	defer lim.mu.Unlock()
 
-	t, tokens := lim.advance(t)
+	tokens := lim.advance(t)
 
 	lim.last = t
 	lim.tokens = tokens
@@ -347,7 +347,7 @@ func (lim *Limiter) reserveN(t time.Time, n int, maxFutureReserve time.Duration)
 		}
 	}
 
-	t, tokens := lim.advance(t)
+	tokens := lim.advance(t)
 
 	// Calculate the remaining number of tokens resulting from the request.
 	tokens -= float64(n)
@@ -380,10 +380,11 @@ func (lim *Limiter) reserveN(t time.Time, n int, maxFutureReserve time.Duration)
 	return r
 }
 
-// advance calculates and returns an updated state for lim resulting from the passage of time.
+// advance calculates and returns an updated number of tokens for lim
+// resulting from the passage of time.
 // lim is not changed.
 // advance requires that lim.mu is held.
-func (lim *Limiter) advance(t time.Time) (newT time.Time, newTokens float64) {
+func (lim *Limiter) advance(t time.Time) (newTokens float64) {
 	last := lim.last
 	if t.Before(last) {
 		last = t
@@ -396,7 +397,7 @@ func (lim *Limiter) advance(t time.Time) (newT time.Time, newTokens float64) {
 	if burst := float64(lim.burst); tokens > burst {
 		tokens = burst
 	}
-	return t, tokens
+	return tokens
 }
 
 // durationFromTokens is a unit conversion function from the number of tokens to the duration
@@ -405,8 +406,15 @@ func (limit Limit) durationFromTokens(tokens float64) time.Duration {
 	if limit <= 0 {
 		return InfDuration
 	}
-	seconds := tokens / float64(limit)
-	return time.Duration(float64(time.Second) * seconds)
+
+	duration := (tokens / float64(limit)) * float64(time.Second)
+
+	// Cap the duration to the maximum representable int64 value, to avoid overflow.
+	if duration > float64(math.MaxInt64) {
+		return InfDuration
+	}
+
+	return time.Duration(duration)
 }
 
 // tokensFromDuration is a unit conversion function from a time duration to the number of tokens
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 3fecefe22..f425d46a4 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -58,7 +58,7 @@ github.com/aws/aws-sdk-go/service/sts/stsiface
 # github.com/davecgh/go-spew v1.1.1
 ## explicit
 github.com/davecgh/go-spew/spew
-# github.com/digitalocean/godo v1.138.0
+# github.com/digitalocean/godo v1.140.0
 ## explicit; go 1.22
 github.com/digitalocean/godo
 github.com/digitalocean/godo/metrics
@@ -307,8 +307,8 @@ golang.org/x/net/http2/hpack
 golang.org/x/net/idna
 golang.org/x/net/internal/timeseries
 golang.org/x/net/trace
-# golang.org/x/oauth2 v0.25.0
-## explicit; go 1.18
+# golang.org/x/oauth2 v0.28.0
+## explicit; go 1.23.0
 golang.org/x/oauth2
 golang.org/x/oauth2/internal
 # golang.org/x/sys v0.28.0
@@ -322,8 +322,8 @@ golang.org/x/text/secure/bidirule
 golang.org/x/text/transform
 golang.org/x/text/unicode/bidi
 golang.org/x/text/unicode/norm
-# golang.org/x/time v0.9.0
-## explicit; go 1.18
+# golang.org/x/time v0.11.0
+## explicit; go 1.23.0
 golang.org/x/time/rate
 # google.golang.org/appengine v1.6.7
 ## explicit; go 1.11


Reviews:
user: login: danaelhe
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T15:40:33Z
user: login: danaelhe
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T15:41:31Z
user: login: amahajan-do
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:51:53Z
user: login: danaelhe
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T18:33:25Z
user: login: danaelhe
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T18:43:30Z
user: login: danaelhe
body: Added a few nitpick comments around documentation, but looks great on my end!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T18:44:59Z
user: login: amahajan-do
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T05:15:52Z

Review comments:
diff_hunk: @@ -0,0 +1,69 @@
+package spaces
+
+import (
+	"context"
+	"log"
+
+	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config"
+
+	"github.com/digitalocean/godo"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+)
+
+func DataSourceDigitalOceanSpacesKey() *schema.Resource {
+	return &schema.Resource{
+		ReadContext: dataSourceSpacesKeyRead,
+
+		Schema: spacesKeySchema(),
+	}
+}
+
+func dataSourceSpacesKeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+	client := meta.(*config.CombinedConfig).GodoClient()
+
+	opts := &godo.ListOptions{
+		Page:    1,
+		PerPage: 200,
+	}
+
+	var key *godo.SpacesKey
+
+	for {
+		keys, resp, err := client.SpacesKeys.List(ctx, opts)
+		if err != nil {
+			return diag.Errorf("Error reading Spaces key: %s", err)
+		}
+
+		for _, k := range keys {
+			if k.Name == d.Get("name").(string) {
+				key = k
+				break
+			}
+		}
+
+		if resp.Links == nil || resp.Links.IsLastPage() {
+			break
+		}
+
+		page, err := resp.Links.CurrentPage()
+		if err != nil {
+			return diag.Errorf("Error reading Spaces key: %s", err)
+		}
+
+		opts.Page = page + 1
+	}
path: digitalocean/spaces/datasource_spaces_key.go
user: login: danaelhe
body: Can we use `client.SpacesKeys.Get(ctx, d.Id())` here too? 
created_at: 2025-03-20T15:40:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+package spaces_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/acceptance"
+	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config"
+
+	"github.com/digitalocean/godo"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+)
+
+func TestAccDataSourceDigitalOceanSpacesKey_basic(t *testing.T) {
+	name := acceptance.RandomTestName()
+	resources, both := testAccDataSourceDigitalOceanSpacesKeyConfig_basic(name)
+
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:                  func() { acceptance.TestAccPreCheck(t) },
+		ProviderFactories:         acceptance.TestAccProviderFactories,
+		CheckDestroy:              testAccCheckDigitalOceanSpacesKeyDestroy,
+		PreventPostDestroyRefresh: true,
+		Steps: []resource.TestStep{
+			{
+				Config: resources,
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("digitalocean_spaces_key.key", "name", name),
+					resource.TestCheckResourceAttr("digitalocean_spaces_key.key", "grant.0.bucket", "my-bucket"),
+					resource.TestCheckResourceAttr("digitalocean_spaces_key.key", "grant.0.permission", "read"),
+				),
+			},
+			{
+				Config: both,
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("data.digitalocean_spaces_key.key", "name", name),
+					resource.TestCheckResourceAttr("data.digitalocean_spaces_key.key", "grant.0.bucket", "my-bucket"),
+					resource.TestCheckResourceAttr("data.digitalocean_spaces_key.key", "grant.0.permission", "read"),
+				),
+			},
+		},
+	})
+}
+
+func testAccDataSourceDigitalOceanSpacesKeyConfig_basic(name string) (string, string) {
+	resources := fmt.Sprintf(`
+resource "digitalocean_spaces_key" "key" {
+  name = "%s"
+  grant {
+    bucket     = "my-bucket"
+    permission = "read"
+  }
+  grant {
+    bucket     = "my-bucket2"
+    permission = "readwrite"
+  }
+}
+`, name)
+
+	both := fmt.Sprintf(`%s
+data "digitalocean_spaces_key" "key" {
+  name = "%s"
+}
+`, resources, name)
+
+	return resources, both
+}
+
+func testAccCheckDigitalOceanSpacesKeyDestroy(s *terraform.State) error {
+	return testAccCheckDigitalOceanSpacesKeyDestroyWithProvider(s, acceptance.TestAccProvider)
+}
+
+func testAccCheckDigitalOceanSpacesKeyDestroyWithProvider(s *terraform.State, provider *schema.Provider) error {
+	for _, rs := range s.RootModule().Resources {
+		if rs.Type != "digitalocean_spaces_key" {
+			continue
+		}
+
+		if rs.Primary.ID == "" {
+			return fmt.Errorf("No Record ID is not set")
+		}
+
+		client := provider.Meta().(*config.CombinedConfig).GodoClient()
+
+		opts := &godo.ListOptions{
+			Page:    1,
+			PerPage: 200,
+		}
+
+		for {
+			keys, resp, err := client.SpacesKeys.List(context.Background(), opts)
+			if err != nil {
+				return fmt.Errorf("Error listing Spaces keys: %s", err)
+			}
+
+			for _, key := range keys {
+				if key.AccessKey == rs.Primary.ID {
+					return fmt.Errorf("Key still exists")
+				}
+			}
+
+			if resp.Links == nil || resp.Links.IsLastPage() {
+				break
+			}
+
+			page, err := resp.Links.CurrentPage()
+			if err != nil {
+				return fmt.Errorf("Error reading Spaces key: %s", err)
+			}
+
+			opts.Page = page + 1
+		}
+
+		return nil
path: digitalocean/spaces/datasource_spaces_key_test.go
user: login: danaelhe
body: `client.SpacesKeys.Get(ctx, d.Id())` here as well? 

created_at: 2025-03-20T15:41:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,69 @@
+package spaces
+
+import (
+	"context"
+	"log"
+
+	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config"
+
+	"github.com/digitalocean/godo"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+)
+
+func DataSourceDigitalOceanSpacesKey() *schema.Resource {
+	return &schema.Resource{
+		ReadContext: dataSourceSpacesKeyRead,
+
+		Schema: spacesKeySchema(),
+	}
+}
+
+func dataSourceSpacesKeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+	client := meta.(*config.CombinedConfig).GodoClient()
+
+	opts := &godo.ListOptions{
+		Page:    1,
+		PerPage: 200,
+	}
+
+	var key *godo.SpacesKey
+
+	for {
+		keys, resp, err := client.SpacesKeys.List(ctx, opts)
+		if err != nil {
+			return diag.Errorf("Error reading Spaces key: %s", err)
+		}
+
+		for _, k := range keys {
+			if k.Name == d.Get("name").(string) {
+				key = k
+				break
+			}
+		}
+
+		if resp.Links == nil || resp.Links.IsLastPage() {
+			break
+		}
+
+		page, err := resp.Links.CurrentPage()
+		if err != nil {
+			return diag.Errorf("Error reading Spaces key: %s", err)
+		}
+
+		opts.Page = page + 1
+	}
path: digitalocean/spaces/datasource_spaces_key.go
user: login: amahajan-do
body: I did update this at first. But when I built the provider and tried to fetch a key using that, it was throwing an error saying that "name" is required and did not allow me to pass the "id" value saying that the value is auto generated. 
created_at: 2025-03-20T15:51:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,70 @@
+---
+page_title: "DigitalOcean: digitalocean_spaces_key"
+subcategory: "Spaces Object Storage"
+---
+
+# digitalocean\_spaces\_key
+
+Provides a key resource for Spaces, DigitalOcean's object storage product.
+
+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.
+
path: docs/resources/spaces_key.md
user: login: danaelhe
body: Maybe add some more information specific to spaces keys here? 
created_at: 2025-03-21T18:33:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+---
+page_title: "DigitalOcean: digitalocean_spaces_key"
+subcategory: "Spaces Object Storage"
+---
+
+# digitalocean\_spaces\_key
+
+Provides a key resource for Spaces, DigitalOcean's object storage product.
+
+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.
+
path: docs/resources/spaces_key.md
user: login: danaelhe
body: Also doesn't hurt to add:
```
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:
```
created_at: 2025-03-21T18:43:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+---
+page_title: "DigitalOcean: digitalocean_spaces_key"
+subcategory: "Spaces Object Storage"
+---
+
+# digitalocean\_spaces\_key
+
+Provides a key resource for Spaces, DigitalOcean's object storage product.
+
+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.
+
path: docs/resources/spaces_key.md
user: login: amahajan-do
body: I removed this part specifically as this can make things confusing because these vars are relevant to the `spaces_bucket` resource and not the `spaces_key` resource. I'll add some more info about spaces key from the official docs.
created_at: 2025-03-24T05:15:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: danaelhe
created_at: 2025-03-20T16:13:11Z
body: Oh this is also missing documentation:
[https://github.com/digitalocean/terraform-provider-digitalocean/tree/main/docs](https://github.com/digitalocean/terraform-provider-digitalocean/tree/main/docs
)
author_association: MEMBER
user: login: amahajan-do
created_at: 2025-03-24T05:29:54Z
body: For posterity, here is the diff against #1309: https://github.com/lee-aaron/terraform-provider-digitalocean/compare/lee-aaron:terraform-provider-digitalocean:support-Spaces-API...amahajan-do:terraform-provider-digitalocean:support-Spaces-API
author_association: CONTRIBUTOR