derailed/k9s · warning

node is already uncordoned

Error message

node is already uncordoned

What it means

The uncordon mirror of the cordon guard: ToggleCordon(ctx, fqn, false) reaches UpdateIfRequired, which reports no change because spec.unschedulable is already false. The DAO returns this error instead of patching the node with an identical spec.

Solutions

  1. Refresh the node and skip the call when node.Spec.Unschedulable is already false
  2. Treat the error as success in idempotent workflows (log-and-continue)
  3. Check current state with kubectl cordon --dry-run equivalent: kubectl get node <n> -o jsonpath='{.spec.unschedulable}'

Example fix

// before
err := n.ToggleCordon(ctx, fqn, false)

// after
node, _ := dao.FetchNode(ctx, factory, fqn)
if node != nil && !node.Spec.Unschedulable {
    return nil // already uncordoned
}
err := n.ToggleCordon(ctx, fqn, false)
Defensive patterns

Strategy: validation

Validate before calling

node, err := dao.FetchNode(ctx, factory, fqn)
if err != nil { return err }
if !node.Spec.Unschedulable {
    return nil // already uncordoned
}
return nodeDAO.ToggleCordon(ctx, fqn, false)

Type guard

func isUncordoned(n *v1.Node) bool {
    return n != nil && !n.Spec.Unschedulable
}

Try / catch

if err := n.ToggleCordon(ctx, fqn, false); err != nil {
    if strings.Contains(err.Error(), "already uncordoned") {
        return nil // idempotent success
    }
    return err
}

Prevention

When it happens

Trigger: Calling ToggleCordon(ctx, fqn, false) on a node that is not cordoned (spec.unschedulable == false), e.g. uncordon after a drain that already uncordoned.

Common situations: Post-drain cleanup scripts that uncordon unconditionally; UI toggles raced against another operator; running uncordon on a fresh node that was never cordoned.

Related errors


AI-assisted analysis of derailed/k9s@2d3ccc6ba2 (2026-08-15). Data as JSON: /api/errors/67c7343397d738e5. Report an issue: GitHub.

Appendix: source

Thrown at internal/dao/node.go:65

	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
}

func (o DrainOptions) toDrainHelper(k kubernetes.Interface, w io.Writer) drain.Helper {

View on GitHub (pinned to 2d3ccc6ba2)