kubernetes/kops · error
failed to delete ssh key %s (ID %s): %s
Error message
failed to delete ssh key %s (ID %s): %s
What it means
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.
Source
Thrown at pkg/resources/digitalocean/resources.go:447
return resourceTrackers
}
func deleteSSHKey(cloud fi.Cloud, t *resources.Resource) error {
c := cloud.(do.DOCloud)
id, err := strconv.Atoi(t.ID)
if err != nil {
return fmt.Errorf("failed to convert ssh key ID %q to int: %s", t.ID, err)
}
klog.V(2).Infof("deleting DO SSH key %q (ID %d)", t.Name, id)
response, err := c.KeysService().DeleteByID(context.TODO(), id)
if err != nil {
if response != nil && response.StatusCode == http.StatusNotFound {
return nil
}
return fmt.Errorf("failed to delete ssh key %s (ID %s): %s", t.Name, t.ID, err)
}
return nil
}
func listVPCs(cloud fi.Cloud, clusterName string) ([]*resources.Resource, error) {
c := cloud.(do.DOCloud)
var resourceTrackers []*resources.Resource
clusterName = do.SafeClusterName(clusterName)
vpcName := "vpc-" + clusterName
vpcs, err := c.GetAllVPCs()
if err != nil {
return nil, fmt.Errorf("failed to list vpcs: %v", err)
}
for _, vpc := range vpcs {View on GitHub (pinned to 4c8573c808)
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
Defensive patterns
Strategy: try-catch
Validate before calling
// Check key existence first; skip if already gone
_, resp, err := c.KeysService().GetByID(context.TODO(), id)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
return nil
}
return fmt.Errorf("cannot fetch ssh key %d: %v", id, err)
} Try / catch
response, err := c.KeysService().DeleteByID(context.TODO(), id)
if err != nil {
if response != nil && response.StatusCode == http.StatusNotFound {
return nil
}
if response != nil && (response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500) {
// retry with exponential backoff
}
return fmt.Errorf("failed to delete ssh key %s (ID %s): %w", t.Name, t.ID, err)
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- failed to delete record for domain %s: %d
- failed to delete load balancer with name %s %v
- failed to list ssh keys: %v
- failed to apply resource record set: %s, err: %s
- failed to list lbs: %v
AI-assisted analysis of kubernetes/kops@4c8573c808 (2026-09-05).
Data as JSON: /api/errors/076c94494f4de6e5.
Report an issue: GitHub.