Repository: digitalocean/terraform-provider-digitalocean

Issue:
Terraform does not clean up docker credentials on destroy
# Bug Report

Terraform does not remove created credentials for container registry on destroy.

---

## Describe the bug
Credentials are created, but are not removed on destroy.

### Affected Resource(s)
- digitalocean_container_registry_docker_credentials.

### Expected Behavior
Terraform delete the created access token

### Actual Behavior
Terraform logs says that the credentials were removed, but the access token is still available on the website (https://cloud.digitalocean.com/account/api/tokens)

### Steps to Reproduce
1. `terraform apply`
2. `terraform destroy`

**Terraform Configuration Files**
```terraform
resource "digitalocean_container_registry" "container_registry" {
  name                   = "my-container-registry-u712345"
  subscription_tier_slug = "basic"
  region                 = var.region
}

resource "digitalocean_container_registry_docker_credentials" "container_registry_credentials" {
  registry_name  = "my-container-registry-u712345"
  expiry_seconds = 365 * 24 * 60 * 60

  depends_on = [
    digitalocean_container_registry.container_registry
  ]
}
```

**Terraform version**
Terraform v1.10.4
on darwin_arm64

provider registry.terraform.io/digitalocean/digitalocean v2.50.0

**Debug Output**
```sh
$ terraform destroy -auto-approve
digitalocean_container_registry.container_registry: Refreshing state... [id=my-container-registry-u712345]
digitalocean_container_registry_docker_credentials.container_registry_credentials: Refreshing state... [id=my-container-registry-u712345]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # digitalocean_container_registry.container_registry will be destroyed
  - resource "digitalocean_container_registry" "container_registry" {
      - created_at             = "2025-05-15 21:06:49 +0000 UTC" -> null
      - endpoint               = "registry.digitalocean.com/my-container-registry-u712345" -> null
      - id                     = "my-container-registry-u712345" -> null
      - name                   = "my-container-registry-u712345" -> null
      - region                 = "ams3" -> null
      - server_url             = "registry.digitalocean.com" -> null
      - storage_usage_bytes    = 0 -> null
      - subscription_tier_slug = "basic" -> null
    }

  # digitalocean_container_registry_docker_credentials.container_registry_credentials will be destroyed
  - resource "digitalocean_container_registry_docker_credentials" "container_registry_credentials" {
      - credential_expiration_time = "2026-05-15T21:06:50Z" -> null
      - docker_credentials         = (sensitive value) -> null
      - expiry_seconds             = 31536000 -> null
      - id                         = "my-container-registry-u712345" -> null
      - registry_name              = "my-container-registry-u712345" -> null
      - write                      = false -> null
    }

Plan: 0 to add, 0 to change, 2 to destroy.
digitalocean_container_registry_docker_credentials.container_registry_credentials: Destroying... [id=my-container-registry-u712345]
digitalocean_container_registry_docker_credentials.container_registry_credentials: Destruction complete after 0s
digitalocean_container_registry.container_registry: Destroying... [id=my-container-registry-u712345]
digitalocean_container_registry.container_registry: Destruction complete after 1s
```



Discussion:

Solution patch:
diff --git a/digitalocean/registry/resource_container_registry_docker_credentials.go b/digitalocean/registry/resource_container_registry_docker_credentials.go
index 90df821f..89bad222 100644
--- a/digitalocean/registry/resource_container_registry_docker_credentials.go
+++ b/digitalocean/registry/resource_container_registry_docker_credentials.go
@@ -124,7 +124,7 @@ func resourceDigitalOceanContainerRegistryDockerCredentialsUpdate(ctx context.Co
 	return nil
 }
 
-type dockerConfig struct {
+type DockerConfig struct {
 	Auths struct {
 		Registry struct {
 			Auth string `json:"auth"`
@@ -134,24 +134,18 @@ type dockerConfig struct {
 
 func resourceDigitalOceanContainerRegistryDockerCredentialsDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
 	configJSON := d.Get("docker_credentials")
-	var config dockerConfig
+	var config DockerConfig
 	err := json.Unmarshal([]byte(configJSON.(string)), &config)
 	if err != nil {
 		return diag.FromErr(err)
 	}
 
-	// The OAuth token is used for both the username and password
-	// and stored as a base64 encoded string.
-	decoded, err := base64.StdEncoding.DecodeString(config.Auths.Registry.Auth)
+	token, err := DecodeToken(config)
 	if err != nil {
-		return diag.FromErr(err)
-	}
-	tokens := strings.Split(string(decoded), ":")
-	if len(tokens) != 2 {
-		return diag.FromErr(errors.New("unable to find OAuth token"))
+		diag.FromErr(err)
 	}
 
-	err = RevokeOAuthToken(tokens[0], oauthTokenRevokeEndpoint)
+	err = RevokeOAuthToken(token, oauthTokenRevokeEndpoint)
 	if err != nil {
 		diag.FromErr(err)
 	}
@@ -160,6 +154,22 @@ func resourceDigitalOceanContainerRegistryDockerCredentialsDelete(ctx context.Co
 	return nil
 }
 
+func DecodeToken(config DockerConfig) (string, error) {
+	// The OAuth token is used for the password and stored as a base64 encoded string.
+	decoded, err := base64.StdEncoding.DecodeString(config.Auths.Registry.Auth)
+	if err != nil {
+		return "", err
+	}
+
+	// The decoded string is in the format "email:token"
+	tokens := strings.Split(string(decoded), ":")
+	if len(tokens) != 2 {
+		return "", errors.New("unable to find OAuth token")
+	}
+
+	return tokens[1], nil
+}
+
 func RevokeOAuthToken(token string, endpoint string) error {
 	data := url.Values{}
 	data.Set("token", token)



Test patch:
diff --git a/digitalocean/registry/resource_container_registry_docker_credentials_test.go b/digitalocean/registry/resource_container_registry_docker_credentials_test.go
index fa4281e4..82342ab7 100644
--- a/digitalocean/registry/resource_container_registry_docker_credentials_test.go
+++ b/digitalocean/registry/resource_container_registry_docker_credentials_test.go
@@ -2,12 +2,15 @@ package registry_test
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+	"net/http"
 	"testing"
 
 	"github.com/digitalocean/godo"
 	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/acceptance"
 	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config"
+	"github.com/digitalocean/terraform-provider-digitalocean/digitalocean/registry"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
 )
@@ -71,18 +74,36 @@ func TestAccDigitalOceanContainerRegistryDockerCredentials_withExpiry(t *testing
 }
 
 func testAccCheckDigitalOceanContainerRegistryDockerCredentialsDestroy(s *terraform.State) error {
-	client := acceptance.TestAccProvider.Meta().(*config.CombinedConfig).GodoClient()
-
 	for _, rs := range s.RootModule().Resources {
 		if rs.Type != "digitalocean_container_registry_docker_credentials" {
 			continue
 		}
 
-		// Try to find the key
-		_, _, err := client.Registry.Get(context.Background())
+		var config registry.DockerConfig
+		configJSON := rs.Primary.Attributes["docker_credentials"]
+		err := json.Unmarshal([]byte(configJSON), &config)
+		if err != nil {
+			return err
+		}
+
+		token, err := registry.DecodeToken(config)
+		if err != nil {
+			return err
+		}
+
+		// Ensure the token was revoked
+		gClient := godo.NewFromToken(token)
+		account, resp, err := gClient.Account.Get(context.Background())
+		if err != nil {
+			if resp != nil && resp.StatusCode == http.StatusUnauthorized {
+				return nil
+			}
+
+			return err
+		}
 
-		if err == nil {
-			return fmt.Errorf("Container Registry still exists")
+		if account != nil {
+			return fmt.Errorf("Docker credentials were not revoked")
 		}
 	}