{"record":{"id":"baf5ffa19f032d77","repo":"derailed/k9s","slug":"node-is-already-cordoned","errorCode":null,"errorMessage":"node is already cordoned","messagePattern":"node is already cordoned","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/dao/node.go","lineNumber":63,"sourceCode":"\t\tslogs.Bool, cordon,\n\t)\n\to, err := FetchNode(context.Background(), n.Factory, fqn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\th, err := drain.NewCordonHelperFromRuntimeObject(o, scheme.Scheme, n.gvr.GVK())\n\tif err != nil {\n\t\tslog.Debug(\"Fail to toggle cordon on node\",\n\t\t\tslogs.FQN, fqn,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn err\n\t}\n\n\tif !h.UpdateIfRequired(cordon) {\n\t\tif cordon {\n\t\t\treturn fmt.Errorf(\"node is already cordoned\")\n\t\t}\n\t\treturn fmt.Errorf(\"node is already uncordoned\")\n\t}\n\tdial, err := n.getFactory().Client().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr, patchErr := h.PatchOrReplace(dial, false)\n\tif patchErr != nil {\n\t\treturn patchErr\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/derailed/k9s/blob/2d3ccc6ba2ce98c3781bfc441bb3e884f072774f/internal/dao/node.go#L45-L81","documentation":"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.","triggerScenarios":"Calling ToggleCordon(ctx, fqn, true) on a node whose spec.unschedulable is already true — double-cordon from stale UI state or unconditional scripts.","commonSituations":"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.","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"],"exampleFix":"// before\nerr := n.ToggleCordon(ctx, fqn, true)\n\n// after\nnode, _ := dao.FetchNode(ctx, factory, fqn)\nif node != nil && node.Spec.Unschedulable {\n    slog.Info(\"node already cordoned, skipping\", slogs.FQN, fqn)\n    return nil\n}\nerr := n.ToggleCordon(ctx, fqn, true)","handlingStrategy":"validation","validationCode":"node, err := dao.FetchNode(ctx, factory, fqn)\nif err != nil { return err }\nif node.Spec.Unschedulable {\n    slog.Info(\"node already cordoned\", slogs.FQN, fqn)\n    return nil\n}\nreturn nodeDAO.ToggleCordon(ctx, fqn, true)","typeGuard":"func isCordoned(n *v1.Node) bool {\n    return n != nil && n.Spec.Unschedulable\n}","tryCatchPattern":"if err := n.ToggleCordon(ctx, fqn, true); err != nil {\n    if strings.Contains(err.Error(), \"already cordoned\") {\n        return nil // idempotent success\n    }\n    return err\n}","preventionTips":["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"],"tags":["kubernetes","node","cordon","idempotency","drain"],"backgroundTag":null,"analyzedSha":"2d3ccc6ba2ce98c3781bfc441bb3e884f072774f","analyzedAt":"2026-08-15T16:09:14.432Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}