derailed/k9s · warning
node is already cordoned
Error message
node is already cordoned
What it means
Node.ToggleCordon builds a drain.CordonHelper and calls UpdateIfRequired(cordon) to detect a no-op before patching. When the node's spec.unschedulable already equals the requested cordoned state, the patch would change nothing, so the DAO returns this error instead of issuing an empty patch.
Solutions
- Refresh the node and skip the call when node.Spec.Unschedulable already equals the desired state
- Treat the error as a benign idempotency signal: log it and continue the surrounding drain/uncordon workflow
- In automation, read spec.unschedulable first and only toggle on difference
Example fix
// before
err := n.ToggleCordon(ctx, fqn, true)
// after
node, _ := dao.FetchNode(ctx, factory, fqn)
if node != nil && node.Spec.Unschedulable {
slog.Info("node already cordoned, skipping", slogs.FQN, fqn)
return nil
}
err := n.ToggleCordon(ctx, fqn, true) Defensive patterns
Strategy: validation
Validate before calling
node, err := dao.FetchNode(ctx, factory, fqn)
if err != nil { return err }
if node.Spec.Unschedulable {
slog.Info("node already cordoned", slogs.FQN, fqn)
return nil
}
return nodeDAO.ToggleCordon(ctx, fqn, true) Type guard
func isCordoned(n *v1.Node) bool {
return n != nil && n.Spec.Unschedulable
} Try / catch
if err := n.ToggleCordon(ctx, fqn, true); err != nil {
if strings.Contains(err.Error(), "already cordoned") {
return nil // idempotent success
}
return err
} Prevention
- Make cordon/uncordon workflows state-aware: read spec.unschedulable before toggling
- Treat both 'already' messages as no-op confirmations, never as hard failures
- After any drain helper runs, uncordon explicitly since drain leaves the node cordoned
When it happens
Trigger: Calling ToggleCordon(ctx, fqn, true) on a node whose spec.unschedulable is already true — double-cordon from stale UI state or unconditional scripts.
Common situations: Node view not refreshed between two cordon attempts; a drain workflow cordons first and the operator also cordons manually; automation that cordon/uncordons on a timer without reading current state.
Related errors
- node is already uncordoned
- expecting a maintainer for
- node is cordoned
- unable to launch shell pod on node
- unable to locate node
AI-assisted analysis of derailed/k9s@2d3ccc6ba2 (2026-08-15).
Data as JSON: /api/errors/baf5ffa19f032d77.
Report an issue: GitHub.
Appendix: source
Thrown at internal/dao/node.go:63
slogs.Bool, cordon,
)
o, err := FetchNode(context.Background(), n.Factory, fqn)
if err != nil {
return err
}
h, err := drain.NewCordonHelperFromRuntimeObject(o, scheme.Scheme, n.gvr.GVK())
if err != nil {
slog.Debug("Fail to toggle cordon on node",
slogs.FQN, fqn,
slogs.Error, err,
)
return err
}
if !h.UpdateIfRequired(cordon) {
if cordon {
return fmt.Errorf("node is already cordoned")
}
return fmt.Errorf("node is already uncordoned")
}
dial, err := n.getFactory().Client().Dial()
if err != nil {
return err
}
err, patchErr := h.PatchOrReplace(dial, false)
if patchErr != nil {
return patchErr
}
if err != nil {
return err
}
return nil
}View on GitHub (pinned to 2d3ccc6ba2)