What happened?
Rolling out the controller (v0.4.1) plus a condition-reporting DaemonSet
(node-problem-detector with custom conditions) simultaneously to a cluster of
long-lived, healthy nodes left one node with a permanently orphaned
readiness.k8s.io/newlyjoinednode:NoSchedule taint.
Two controllers raced on the same node within the same second, on the controller's
first-ever reconcile sweep:
- The rule controller evaluated the node before the reporter DaemonSet had
stamped any of the rule's conditions, so every condition evaluated as
observed: "Unknown" vs required: "False" → allConditionsSatisfied: false
→ it added the taint.
- The node controller concurrently ran the grandfathering path for the
pre-existing node and marked bootstrap completed
(readiness.k8s.io/bootstrap-completed-<rule-uid> annotation written).
- Every subsequent reconcile of that node now logs
Skipping bootstrap-only rule - already completed, which short-circuits the
entire rule — including taint removal.
End state (still true hours later): node is healthy, all rule conditions read
False (satisfied), the bootstrap-completed annotation is present, and the
rule's NoSchedule taint is still on the node. Nothing will ever remove it; it
had to be removed manually.
Other nodes tainted in the same initial sweep were cleaned up correctly — they
won the race (taint removed before the completion mark landed). Only the
interleaving order differed.
Steps to Reproduce
-
Cluster with existing Ready worker nodes that do not yet carry the
rule's condition types (reporter not yet deployed).
-
Deploy, at effectively the same time, a bootstrap-only NodeReadinessRule
and the DaemonSet that reports its conditions:
apiVersion: readiness.node.x-k8s.io/v1alpha1
kind: NodeReadinessRule
metadata:
name: newly-joined-node-rule
spec:
enforcementMode: bootstrap-only
conditions:
- type: CustomCheckA
requiredStatus: "False"
- type: CustomCheckB
requiredStatus: "False"
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
taint:
key: readiness.k8s.io/newlyjoinednode
value: "true"
effect: NoSchedule
-
On the controller's first sweep, nodes evaluate Unknown on all conditions
and get tainted, while the node controller concurrently grandfathers them as
bootstrap-completed. Depending on interleaving, some nodes end up completed
with the taint still present, and it is never removed.
The race is timing-dependent; with ~20 matching nodes, one node hit it.
Log evidence (sanitized hostnames, otherwise verbatim)
07:43:10Z DEBUG Condition evaluation ... "node": "node-01", "conditionType": "CustomCheckA", "observed": "Unknown", "effective": "Unknown", "required": "False", "satisfied": false, "reconcileID": "4593ceed-..."
07:43:10Z INFO Evaluation result ... "node": "node-01", "allConditionsSatisfied": false, "hasTaint": false, "reconcileID": "4593ceed-..."
07:43:10Z INFO Adding taint ... "node": "node-01", "taint": "readiness.k8s.icileID": "4593ceed-..." <- rule controller
07:43:10Z INFO Marked bootstrap completed ... "controllerKind": "Node", "node": "node-01", "reconcileID": "bd456526-..." <- node controller, same second
07:43:10Z INFO Skipping bootstrap-only rule - already completed ... "node": "node-01"-..." <- all later reconciles
Node state hours later: conditions all False (healthy, heartbeats current),
readiness.k8s.io/bootstrap-completed- annotation present, taint
readiness.k8s.io/newlyjoinednode:NoSchedule still present.
Expected Behavior
Either of these invariants would prevent the orphaned taint:
markBootstrapCompleted removes (or verifies absence of) any taint owned by
the rule before/atomically with writing the completion annotation, or
- the skip-if-completed branch first checks whether the rule's taint is still
present on the node and lifts it before skipping.
Related: #207 notes that completion-marking is intended to happen during taint
removal. This report is the mirror image — the grandfathering path marks
completion via the node controller while the rule controller's taint from a
concurrent reconcile is still in flight, so the "completed" state and the taint
coexist.
Controller Version / Image Tag
v0.4.1
Kubernetes Version
v1.34.x
Controller Logs
No response
Additional Environment Details
No response
What happened?
Rolling out the controller (v0.4.1) plus a condition-reporting DaemonSet
(node-problem-detector with custom conditions) simultaneously to a cluster of
long-lived, healthy nodes left one node with a permanently orphaned
readiness.k8s.io/newlyjoinednode:NoScheduletaint.Two controllers raced on the same node within the same second, on the controller's
first-ever reconcile sweep:
stamped any of the rule's conditions, so every condition evaluated as
observed: "Unknown"vsrequired: "False"→allConditionsSatisfied: false→ it added the taint.
pre-existing node and marked bootstrap completed
(
readiness.k8s.io/bootstrap-completed-<rule-uid>annotation written).Skipping bootstrap-only rule - already completed, which short-circuits theentire rule — including taint removal.
End state (still true hours later): node is healthy, all rule conditions read
False(satisfied), the bootstrap-completed annotation is present, and therule's
NoScheduletaint is still on the node. Nothing will ever remove it; ithad to be removed manually.
Other nodes tainted in the same initial sweep were cleaned up correctly — they
won the race (taint removed before the completion mark landed). Only the
interleaving order differed.
Steps to Reproduce
Cluster with existing
Readyworker nodes that do not yet carry therule's condition types (reporter not yet deployed).
Deploy, at effectively the same time, a bootstrap-only
NodeReadinessRuleand the DaemonSet that reports its conditions:
On the controller's first sweep, nodes evaluate Unknown on all conditions
and get tainted, while the node controller concurrently grandfathers them as
bootstrap-completed. Depending on interleaving, some nodes end up completed
with the taint still present, and it is never removed.
The race is timing-dependent; with ~20 matching nodes, one node hit it.
Log evidence (sanitized hostnames, otherwise verbatim)
07:43:10Z DEBUG Condition evaluation ... "node": "node-01", "conditionType": "CustomCheckA", "observed": "Unknown", "effective": "Unknown", "required": "False", "satisfied": false, "reconcileID": "4593ceed-..."
07:43:10Z INFO Evaluation result ... "node": "node-01", "allConditionsSatisfied": false, "hasTaint": false, "reconcileID": "4593ceed-..."
07:43:10Z INFO Adding taint ... "node": "node-01", "taint": "readiness.k8s.icileID": "4593ceed-..." <- rule controller
07:43:10Z INFO Marked bootstrap completed ... "controllerKind": "Node", "node": "node-01", "reconcileID": "bd456526-..." <- node controller, same second
07:43:10Z INFO Skipping bootstrap-only rule - already completed ... "node": "node-01"-..." <- all later reconciles
Node state hours later: conditions all False (healthy, heartbeats current),
readiness.k8s.io/bootstrap-completed- annotation present, taint
readiness.k8s.io/newlyjoinednode:NoSchedule still present.
Expected Behavior
Either of these invariants would prevent the orphaned taint:
markBootstrapCompletedremoves (or verifies absence of) any taint owned bythe rule before/atomically with writing the completion annotation, or
present on the node and lifts it before skipping.
Related: #207 notes that completion-marking is intended to happen during taint
removal. This report is the mirror image — the grandfathering path marks
completion via the node controller while the rule controller's taint from a
concurrent reconcile is still in flight, so the "completed" state and the taint
coexist.
Controller Version / Image Tag
v0.4.1
Kubernetes Version
v1.34.x
Controller Logs
No response
Additional Environment Details
No response