{"record":{"id":"67c7343397d738e5","repo":"derailed/k9s","slug":"node-is-already-uncordoned","errorCode":null,"errorMessage":"node is already uncordoned","messagePattern":"node is already uncordoned","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/dao/node.go","lineNumber":65,"sourceCode":"\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}\n\nfunc (o DrainOptions) toDrainHelper(k kubernetes.Interface, w io.Writer) drain.Helper {","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/derailed/k9s/blob/2d3ccc6ba2ce98c3781bfc441bb3e884f072774f/internal/dao/node.go#L47-L83","documentation":"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.","triggerScenarios":"Calling ToggleCordon(ctx, fqn, false) on a node that is not cordoned (spec.unschedulable == false), e.g. uncordon after a drain that already uncordoned.","commonSituations":"Post-drain cleanup scripts that uncordon unconditionally; UI toggles raced against another operator; running uncordon on a fresh node that was never cordoned.","solutions":["Refresh the node and skip the call when node.Spec.Unschedulable is already false","Treat the error as success in idempotent workflows (log-and-continue)","Check current state with kubectl cordon --dry-run equivalent: kubectl get node <n> -o jsonpath='{.spec.unschedulable}'"],"exampleFix":"// before\nerr := n.ToggleCordon(ctx, fqn, false)\n\n// after\nnode, _ := dao.FetchNode(ctx, factory, fqn)\nif node != nil && !node.Spec.Unschedulable {\n    return nil // already uncordoned\n}\nerr := n.ToggleCordon(ctx, fqn, false)","handlingStrategy":"validation","validationCode":"node, err := dao.FetchNode(ctx, factory, fqn)\nif err != nil { return err }\nif !node.Spec.Unschedulable {\n    return nil // already uncordoned\n}\nreturn nodeDAO.ToggleCordon(ctx, fqn, false)","typeGuard":"func isUncordoned(n *v1.Node) bool {\n    return n != nil && !n.Spec.Unschedulable\n}","tryCatchPattern":"if err := n.ToggleCordon(ctx, fqn, false); err != nil {\n    if strings.Contains(err.Error(), \"already uncordoned\") {\n        return nil // idempotent success\n    }\n    return err\n}","preventionTips":["Unconditional uncordon scripts should swallow the 'already uncordoned' case as success","Refresh node state right before toggling in concurrent environments (two operators)","Verify final state with kubectl get node <n> -o jsonpath='{.spec.unschedulable}' after operations"],"tags":["kubernetes","node","uncordon","idempotency","drain"],"backgroundTag":null,"analyzedSha":"2d3ccc6ba2ce98c3781bfc441bb3e884f072774f","analyzedAt":"2026-08-15T16:09:14.432Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}