{"record":{"id":"6e41be5e6d02a818","repo":"vitessio/vitess","slug":"w-cannot-delete-shards-in-s","errorCode":null,"errorMessage":"%w: cannot delete shards in %s","messagePattern":"%w: cannot delete shards in (.+?)","errorType":"validation","errorClass":"ErrUnauthorized","httpStatus":null,"severity":"error","filePath":"go/vt/vtadmin/api.go","lineNumber":672,"sourceCode":"\t}\n\n\tc, err := api.getClusterForRequest(req.ClusterId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c.DeleteKeyspace(ctx, req.Options)\n}\n\n// DeleteShards is part of the vtadminpb.VTAdminServer interface.\nfunc (api *API) DeleteShards(ctx context.Context, req *vtadminpb.DeleteShardsRequest) (*vtctldatapb.DeleteShardsResponse, error) {\n\tspan, ctx := trace.NewSpan(ctx, \"API.DeleteShards\")\n\tdefer span.Finish()\n\n\tspan.Annotate(\"cluster_id\", req.ClusterId)\n\n\tif !api.authz.IsAuthorized(ctx, req.ClusterId, rbac.ShardResource, rbac.DeleteAction) {\n\t\treturn nil, fmt.Errorf(\"%w: cannot delete shards in %s\", errors.ErrUnauthorized, req.ClusterId)\n\t}\n\n\tc, err := api.getClusterForRequest(req.ClusterId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c.DeleteShards(ctx, req.Options)\n}\n\n// DeleteTablet is part of the vtadminpb.VTAdminServer interface.\nfunc (api *API) DeleteTablet(ctx context.Context, req *vtadminpb.DeleteTabletRequest) (*vtadminpb.DeleteTabletResponse, error) {\n\tspan, ctx := trace.NewSpan(ctx, \"API.DeleteTablet\")\n\tdefer span.Finish()\n\n\ttablet, c, err := api.getTabletForAction(ctx, span, rbac.DeleteAction, req.Alias, req.ClusterIds)\n\tif err != nil {\n\t\treturn nil, err","sourceCodeStart":654,"sourceCodeEnd":690,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/vt/vtadmin/api.go#L654-L690","documentation":"VTAdmin's DeleteShards RPC returns this when RBAC denies the caller the 'delete' action on the Shard resource in the requested cluster. The API fails fast before contacting the cluster, wrapping errors.ErrUnauthorized. Shard deletion is deliberately gated separately from shard creation.","triggerScenarios":"Calling DeleteShards (DELETE /shards) for a cluster where the caller's role omits shard-delete permission.","commonSituations":"Decommissioning workflows run by service accounts lacking delete grants; RBAC configs granting wildcard on keyspaces but not shards; multi-cluster setups where the role covers only some cluster IDs.","solutions":["Add 'delete' to the shard resource actions for the caller's role in the RBAC config","Ensure the role's cluster scope includes the target cluster ID (wildcard if needed)","Restart vtadmin so the updated RBAC policy is loaded"],"exampleFix":"// before\n  - resource: shard\n    actions: [get, create]\n// after\n  - resource: shard\n    actions: [get, create, delete]","handlingStrategy":"validation","validationCode":"const canDeleteShards = permissions.some(rule => rule.resource === 'shard' && (rule.actions.includes('delete') || rule.actions.includes('*')) && (rule.clusters.includes(clusterId) || rule.clusters.includes('*')));\nif (!canDeleteShards) throw new Error('RBAC denies shard delete in ' + clusterId);","typeGuard":"function isShardDeleteDenied(err: unknown): boolean {\n  return err instanceof Error && err.message.includes('cannot delete shards');\n}","tryCatchPattern":"try {\n  await deleteShards(clusterId, shards);\n} catch (err) {\n  if (String(err).includes('cannot delete shards')) {\n    escalateToAdmin('shard:delete grant needed for ' + clusterId);\n  } else {\n    throw err;\n  }\n}","preventionTips":["Grant shard delete explicitly for decommissioning tooling","Use wildcard cluster scope deliberately for ops roles only","Restart vtadmin to load RBAC changes and verify with a dry-run call"],"tags":["rbac","authorization","vtadmin","shard","deletion"],"backgroundTag":"unauthorized-rbac-action","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}