{"record":{"id":"47df9d3b195fbc83","repo":"kubernetes/kops","slug":"failed-to-delete-volume-s-err-s","errorCode":null,"errorMessage":"failed to delete volume: %s, err: %s","messagePattern":"failed to delete volume: (.+?), err: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/resources/digitalocean/resources.go","lineNumber":299,"sourceCode":"\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)\n\n\t_, err := c.DomainService().DeleteRecord(context.TODO(), domain, record.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete record for domain %s: %d\", domain, record.ID)\n\t}\n\n\treturn nil\n}\n\nfunc deleteLoadBalancer(cloud fi.Cloud, t *resources.Resource) error {","sourceCodeStart":281,"sourceCodeEnd":317,"githubUrl":"https://github.com/kubernetes/kops/blob/4c8573c808a73d578c5eadc86d410646ea0b0d73/pkg/resources/digitalocean/resources.go#L281-L317","documentation":"deleteVolume wraps the godo VolumeService.DeleteVolume failure after detaching all droplets. It fires when DO refuses to delete the volume — commonly because a detach is still in progress or the volume is still attached; detached-already (404) cases are skipped earlier in the function.","triggerScenarios":"DeleteVolume returns 404 (already deleted), 403 (scope), or an API/network error; deletion can also fail if DO still considers the volume attached.","commonSituations":"Volume deleted manually before kops delete; detach reported complete but DO hasn't propagated; token lacking volume delete scope; transient API failure.","solutions":["Check the DigitalOcean API token and volume delete permissions","Verify the volume is fully detached, then retry","404 means the volume was already deleted — re-run discovery"],"exampleFix":"// before\n# delete refused: volume still marked attached\n// after\n# retry after detach propagation, or:\ndoctl compute volume delete <volume-id>\nkops delete cluster --cloud digitalocean ...","handlingStrategy":"retry","validationCode":"vol, resp, err := c.VolumeService().GetVolume(context.TODO(), t.ID)\nif resp != nil && resp.StatusCode == http.StatusNotFound {\n\treturn nil // already deleted\n}\nif err != nil { return err }\n_ = vol","typeGuard":null,"tryCatchPattern":"if _, err := c.VolumeService().DeleteVolume(context.TODO(), t.ID); err != nil {\n\tvar gerr *godo.ErrorResponse\n\tif errors.As(err, &gerr) && gerr.Response.StatusCode == http.StatusNotFound {\n\t\treturn nil // idempotent\n\t}\n\treturn fmt.Errorf(\"failed to delete volume: %s, err: %w\", t.ID, err)\n}","preventionTips":["Treat 404 as idempotent success in delete paths","Add a small delay after detach before delete to let attach-state propagate","Ensure token has volume delete scope"],"tags":["digitalocean","volume","api-error"],"backgroundTag":"cloud-resource-delete-failed","analyzedSha":"4c8573c808a73d578c5eadc86d410646ea0b0d73","analyzedAt":"2026-09-05T04:13:19.212Z","contentChangedAt":"2026-09-05T04:13:19.212Z","schemaVersion":2},"datasetVersion":"2026-09-12T07:17:12.445Z"}