{"record":{"id":"66368e69eddb5a78","repo":"kubernetes/kops","slug":"failed-to-detach-volume-s-s","errorCode":null,"errorMessage":"failed to detach volume %s: %s","messagePattern":"failed to detach volume (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/resources/digitalocean/resources.go","lineNumber":289,"sourceCode":"\t_, err := c.VPCsService().Delete(context.TODO(), t.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete VPC %s (ID %s): %s\", t.Name, t.ID, err)\n\t}\n\n\treturn nil\n}\n\nfunc deleteVolume(cloud fi.Cloud, t *resources.Resource) error {\n\tc := cloud.(do.DOCloud)\n\tvolume := t.Obj.(godo.Volume)\n\tfor _, dropletID := range volume.DropletIDs {\n\t\taction, resp, err := c.VolumeActionService().DetachByDropletID(context.TODO(), volume.ID, dropletID)\n\t\tif err != nil {\n\t\t\tif resp != nil && resp.StatusCode == http.StatusNotFound {\n\t\t\t\t// Volume is already detached, nothing to do.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"failed to detach volume %s: %s\", volume.ID, err)\n\t\t}\n\n\t\tif err := waitForDetach(c, action); err != nil {\n\t\t\treturn fmt.Errorf(\"error while waiting for volume %s to detach: %s\", volume.ID, err)\n\t\t}\n\t}\n\n\t_, err := c.VolumeService().DeleteVolume(context.TODO(), t.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete volume: %s, err: %s\", t.ID, err)\n\t}\n\n\treturn nil\n}\n\nfunc deleteRecord(cloud fi.Cloud, domain string, t *resources.Resource) error {\n\tc := cloud.(do.DOCloud)\n\trecord := t.Obj.(godo.DomainRecord)","sourceCodeStart":271,"sourceCodeEnd":307,"githubUrl":"https://github.com/kubernetes/kops/blob/4c8573c808a73d578c5eadc86d410646ea0b0d73/pkg/resources/digitalocean/resources.go#L271-L307","documentation":"deleteVolume attempts VolumeActionService().DetachByDropletID for each attached droplet; any non-404 detach error is wrapped here. Volume 404 during detach is treated as already-detached and skipped; anything else aborts.","triggerScenarios":"DetachByDropletID returns an error with resp.StatusCode != 404: API auth failure, invalid droplet ID, action refused by DO (e.g. volume busy), or network/5xx error.","commonSituations":"Volume attached to a droplet DO won't detach from (state mismatch); token lacking volume action scope; API outage mid-teardown; stale attachment records.","solutions":["Retry the operation; detach actions are often transient failures","Verify the volume's actual attachments (doctl compute volume list) and detach manually if needed","Check token has volume action write scope","Inspect wrapped error for 409/422 hints about why DO refused the detach"],"exampleFix":"// before\n# detach refused for stale attachment\n// after\ndoctl compute volume detach <volume-id> --droplet-id <droplet-id>\nkops delete cluster --cloud digitalocean ...","handlingStrategy":"retry","validationCode":"for _, d := range dropletIDs {\n\tif _, _, err := c.DropletsService().Get(context.TODO(), d); err != nil {\n\t\treturn fmt.Errorf(\"droplet %d not found for detach\", d)\n\t}\n}","typeGuard":null,"tryCatchPattern":"action, resp, err := c.VolumeActionService().DetachByDropletID(ctx, volume.ID, dropletID)\nif err != nil {\n\tif resp != nil && resp.StatusCode == http.StatusNotFound { continue }\n\tif isRetryable(err) { retryWithBackoff(...) }\n\treturn fmt.Errorf(\"failed to detach volume %s: %w\", volume.ID, err)\n}","preventionTips":["Keep the retry/backoff wrapper for detach actions","Verify volume attachments before teardown","Ensure token can perform volume actions"],"tags":["digitalocean","volume","storage"],"backgroundTag":"volume-detach-failed","analyzedSha":"4c8573c808a73d578c5eadc86d410646ea0b0d73","analyzedAt":"2026-09-05T04:13:19.212Z","contentChangedAt":"2026-09-05T04:13:19.212Z","schemaVersion":2},"datasetVersion":"2026-09-12T12:17:11.808Z"}