{"record":{"id":"73b1ce1744b5c9e9","repo":"kubernetes/kops","slug":"failed-to-delete-droplet-d-err-s","errorCode":null,"errorMessage":"failed to delete droplet: %d, err: %s","messagePattern":"failed to delete droplet: (.+?), err: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/resources/digitalocean/resources.go","lineNumber":263,"sourceCode":"\n\t\t\tresourceTracker.Blocks = blocks\n\t\t\tresourceTrackers = append(resourceTrackers, resourceTracker)\n\t\t}\n\t}\n\n\treturn resourceTrackers, nil\n}\n\nfunc deleteDroplet(cloud fi.Cloud, t *resources.Resource) error {\n\tc := cloud.(do.DOCloud)\n\tdropletID, err := strconv.Atoi(t.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert droplet ID to int: %s\", err)\n\t}\n\n\t_, err = c.DropletsService().Delete(context.TODO(), dropletID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete droplet: %d, err: %s\", dropletID, err)\n\t}\n\n\treturn nil\n}\n\nfunc deleteVPC(cloud fi.Cloud, t *resources.Resource) error {\n\tc := cloud.(do.DOCloud)\n\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)","sourceCodeStart":245,"sourceCodeEnd":281,"githubUrl":"https://github.com/kubernetes/kops/blob/4c8573c808a73d578c5eadc86d410646ea0b0d73/pkg/resources/digitalocean/resources.go#L245-L281","documentation":"deleteDroplet wraps the godo DropletsService.Delete failure during cluster teardown. It first converts the resource ID string to an int (separate error), then deletes; this error fires when DO refuses the droplet deletion — still-attached resources, permission, or the droplet already gone with a non-404 error.","triggerScenarios":"c.DropletsService().Delete(context.TODO(), dropletID) errors: droplet already gone (404), token lacking write scope (403), network/API failure.","commonSituations":"Droplet deleted out-of-band then kops delete re-run; expired token after cluster creation; DO API transient failure mid-teardown.","solutions":["Check the DigitalOcean API token and droplet delete permissions","Retry after transient errors","Verify the droplet ID is valid; 404 means it was already deleted"],"exampleFix":"// before\n# 403 insufficient scope\n// after\nexport DIGITALOCEAN_ACCESS_TOKEN=<token with droplet:write scope>\nkops delete cluster --cloud digitalocean ...","handlingStrategy":"retry","validationCode":"droplet, resp, err := c.DropletsService().Get(context.TODO(), dropletID)\nif resp != nil && resp.StatusCode == http.StatusNotFound {\n\treturn nil // already deleted\n}","typeGuard":null,"tryCatchPattern":"if _, err := c.DropletsService().Delete(context.TODO(), dropletID); err != nil {\n\tvar gerr *godo.ErrorResponse\n\tif errors.As(err, &gerr) && gerr.Response.StatusCode == http.StatusNotFound {\n\t\treturn nil // idempotent: already gone\n\t}\n\treturn fmt.Errorf(\"failed to delete droplet: %d, err: %w\", dropletID, err)\n}","preventionTips":["Make deletes idempotent by treating 404 as success","Verify token write scope before teardown runs","Avoid deleting droplets out-of-band while kops manages them"],"tags":["digitalocean","droplet","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-12T12:17:11.808Z"}