{"record":{"id":"076c94494f4de6e5","repo":"kubernetes/kops","slug":"failed-to-delete-ssh-key-s-id-s-s","errorCode":null,"errorMessage":"failed to delete ssh key %s (ID %s): %s","messagePattern":"failed to delete ssh key (.+?) \\(ID (.+?)\\): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/resources/digitalocean/resources.go","lineNumber":447,"sourceCode":"\n\treturn resourceTrackers\n}\n\nfunc deleteSSHKey(cloud fi.Cloud, t *resources.Resource) error {\n\tc := cloud.(do.DOCloud)\n\n\tid, err := strconv.Atoi(t.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert ssh key ID %q to int: %s\", t.ID, err)\n\t}\n\n\tklog.V(2).Infof(\"deleting DO SSH key %q (ID %d)\", t.Name, id)\n\tresponse, err := c.KeysService().DeleteByID(context.TODO(), id)\n\tif err != nil {\n\t\tif response != nil && response.StatusCode == http.StatusNotFound {\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"failed to delete ssh key %s (ID %s): %s\", t.Name, t.ID, err)\n\t}\n\n\treturn nil\n}\n\nfunc listVPCs(cloud fi.Cloud, clusterName string) ([]*resources.Resource, error) {\n\tc := cloud.(do.DOCloud)\n\tvar resourceTrackers []*resources.Resource\n\n\tclusterName = do.SafeClusterName(clusterName)\n\tvpcName := \"vpc-\" + clusterName\n\n\tvpcs, err := c.GetAllVPCs()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list vpcs: %v\", err)\n\t}\n\n\tfor _, vpc := range vpcs {","sourceCodeStart":429,"sourceCodeEnd":465,"githubUrl":"https://github.com/kubernetes/kops/blob/4c8573c808a73d578c5eadc86d410646ea0b0d73/pkg/resources/digitalocean/resources.go#L429-L465","documentation":"deleteSSHKey wraps a non-404 error from the DigitalOcean SSH Keys DeleteByID API. 404 is treated as success (key already gone); anything else — auth failure, forbidden scope, rate limit, network — produces this error while deleting an SSH key during cluster teardown. The key name, ID, and underlying error are all included.","triggerScenarios":"KeysService().DeleteByID returns an error that is not HTTP 404: 401 invalid token, 403 token lacks write scope, 429 rate limited, 5xx DO server error, or network failure.","commonSituations":"Read-only API token used for cluster deletion; DO API incident; rate limiting when deleting many clusters concurrently; key already removed by another process (that path returns nil, so this error implies something other than missing key).","solutions":["Check the wrapped error's HTTP status; verify the token has write scope (doctl compute ssh-key delete <id> as a test)","Retry after backoff on 429/5xx; teardown is idempotent thanks to the 404 pass-through","If the token is read-only, regenerate/reuse a token with full(write) scope and rerun kOps delete cluster","Check DO status page for ongoing incidents before further retries"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Check key existence first; skip if already gone\n_, resp, err := c.KeysService().GetByID(context.TODO(), id)\nif err != nil {\n\tif resp != nil && resp.StatusCode == http.StatusNotFound {\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"cannot fetch ssh key %d: %v\", id, err)\n}","typeGuard":null,"tryCatchPattern":"response, err := c.KeysService().DeleteByID(context.TODO(), id)\nif err != nil {\n\tif response != nil && response.StatusCode == http.StatusNotFound {\n\t\treturn nil\n\t}\n\tif response != nil && (response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500) {\n\t\t// retry with exponential backoff\n\t}\n\treturn fmt.Errorf(\"failed to delete ssh key %s (ID %s): %w\", t.Name, t.ID, err)\n}","preventionTips":["Use a token with write scope for delete cluster operations","Retry 429/5xx with backoff; 404 is already handled as success","Verify token health with doctl account get before long teardown runs","Keep teardown operations sequential or rate-limited to avoid throttling"],"tags":["digitalocean","ssh","api-error","destructive-ops"],"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"}