|
| 1 | +# Karpenter integration |
| 2 | + |
| 3 | +bootc-operator is aware of [Karpenter](https://karpenter.sh/) and coordinates |
| 4 | +with it during node updates to prevent unnecessary node replacement, |
| 5 | +voluntary disruption, and concurrent reboot handling. |
| 6 | + |
| 7 | +## Why this matters |
| 8 | + |
| 9 | +Karpenter monitors nodes for health and may decide to replace or disrupt a |
| 10 | +node that appears unhealthy or underutilized — for example, one that is |
| 11 | +cordoned, being drained, or not yet Ready after a reboot. All of those |
| 12 | +conditions arise naturally during a bootc-operator update window, so without |
| 13 | +coordination Karpenter could race with the operator and terminate a node that |
| 14 | +is in the middle of a legitimate OS update. |
| 15 | + |
| 16 | +Karpenter respects two node-level annotations: |
| 17 | + |
| 18 | +- `karpenter.sh/do-not-repair: "true"` — prevents Node Auto Repair from |
| 19 | + replacing a node that has unhealthy conditions. |
| 20 | +- `karpenter.sh/do-not-disrupt: "true"` — prevents voluntary disruption |
| 21 | + actions (consolidation, drift) from selecting this node. |
| 22 | + |
| 23 | +> **Note:** `karpenter.sh/do-not-repair` was introduced in Karpenter v1.15.0. |
| 24 | +> That is the minimum required version for full bootc-operator Karpenter integration. |
| 25 | +
|
| 26 | +bootc-operator sets both annotations automatically on Karpenter-managed nodes |
| 27 | +for the duration of the full update lifecycle — from image staging through |
| 28 | +node reboot and health verification. Where the associated `NodeClaim` exists, |
| 29 | +it also sets the annotations there. This is best-effort coordination, not a |
| 30 | +distributed lock. |
| 31 | + |
| 32 | +## How it works |
| 33 | + |
| 34 | +When a Karpenter-owned node enters the `Staging` state (the daemon is pulling |
| 35 | +the new image), bootc-operator immediately sets `do-not-repair` and |
| 36 | +`do-not-disrupt` on the Node and, where available, the associated NodeClaim. |
| 37 | +This protection persists through slot assignment, drain, reboot, and post-boot |
| 38 | +health verification. |
| 39 | + |
| 40 | +Before assigning a reboot slot or starting a drain, the controller checks |
| 41 | +whether Karpenter has already committed any lifecycle operation that would |
| 42 | +conflict with the bootc update. It checks: |
| 43 | + |
| 44 | +- Node taints `karpenter.sh/rebooting:NoSchedule` and |
| 45 | + `karpenter.sh/disrupted:NoSchedule` |
| 46 | +- Node `DeletionTimestamp` (node is being terminated) |
| 47 | +- NodeClaim `DeletionTimestamp` (NodeClaim is being deleted) |
| 48 | +- NodeClaim `status.conditions`: `Rebooting=True`, `DisruptionReason=True`, |
| 49 | + or `InstanceTerminating=True` |
| 50 | + |
| 51 | +If any of these signals is active, bootc-operator defers and retries. It |
| 52 | +never removes a Karpenter taint. |
| 53 | + |
| 54 | +If the Karpenter NodeClaim owner reference is present but the NodeClaim object |
| 55 | +returns a 404, bootc-operator treats this as unsafe (the NodeClaim may be |
| 56 | +mid-deletion) and defers reboot-slot assignment and drain with a short requeue |
| 57 | +rather than proceeding. During `Staging`, it still applies the Node-level |
| 58 | +annotations, but skips NodeClaim annotation changes while the NodeClaim is |
| 59 | +unavailable. |
| 60 | + |
| 61 | +If none of those signals is active, the controller takes the following steps: |
| 62 | + |
| 63 | +1. For each annotation (`do-not-repair`, `do-not-disrupt`): if the annotation |
| 64 | + is **not** already set to `"true"`, the controller sets it (value `"true"`) |
| 65 | + on the Node and, where available, the NodeClaim. It records ownership |
| 66 | + separately on the `BootcNode` object: |
| 67 | + - `bootc.dev/owns-karpenter-do-not-repair: "true"` |
| 68 | + - `bootc.dev/owns-karpenter-do-not-disrupt: "true"` |
| 69 | + - `bootc.dev/owns-karpenter-nodeclaim-do-not-repair: "true"` |
| 70 | + - `bootc.dev/owns-karpenter-nodeclaim-do-not-disrupt: "true"` |
| 71 | +2. The controller proceeds through the normal update flow: drain, reboot, and |
| 72 | + image verification. |
| 73 | +3. When the slot is freed (node is Ready and running the new image), the |
| 74 | + controller removes only annotations that it still owns and whose value is |
| 75 | + still `"true"`. It never removes annotations it did not add. |
| 76 | + |
| 77 | +## NodeClaim annotations are supplemental |
| 78 | + |
| 79 | +NodeClaim annotations (`do-not-repair`, `do-not-disrupt`) are a supplemental |
| 80 | +signal, not a distributed lock. By the time bootc-operator writes them, |
| 81 | +Karpenter may already have selected or queued a disruption for the registered |
| 82 | +Node. The authoritative disruption checks are the Node-level taint, the |
| 83 | +NodeClaim deletion timestamp, and the NodeClaim status conditions — the |
| 84 | +controller checks these before both slot assignment and drain. |
| 85 | + |
| 86 | +## NodeClaim watch registration |
| 87 | + |
| 88 | +When the controller starts, it checks whether the Karpenter `NodeClaim` API is |
| 89 | +available. If it is, the controller registers a watch for NodeClaim events and |
| 90 | +uses changes to the associated NodeClaim to requeue its owning |
| 91 | +`BootcNodePool`. This allows changes such as Karpenter starting or completing |
| 92 | +a reboot to be observed promptly. |
| 93 | + |
| 94 | +The watch is registered only during startup. If Karpenter is installed after |
| 95 | +bootc-operator has started, the operator does not add the watch dynamically; |
| 96 | +restart the bootc-operator controller after installing Karpenter. As a fallback, |
| 97 | +the controller schedules a periodic requeue (every 30 seconds) whenever |
| 98 | +Karpenter-owned nodes are active, so that missed NodeClaim events do not |
| 99 | +stall progress indefinitely. |
| 100 | + |
| 101 | +The controller represents NodeClaims with Kubernetes `Unstructured` objects |
| 102 | +instead of importing Karpenter's Go API types. This keeps Karpenter an optional |
| 103 | +integration and avoids adding a Karpenter API dependency or scheme registration |
| 104 | +to bootc-operator. It also lets clusters without the NodeClaim CRD continue to |
| 105 | +run normally. The tradeoff is that the small set of fields used here, such as |
| 106 | +`status.nodeName` and `status.conditions`, is checked at runtime rather than |
| 107 | +being enforced by Go types. |
| 108 | + |
| 109 | +## Pre-existing annotations |
| 110 | + |
| 111 | +If a Karpenter annotation is already present on the Node with a value of |
| 112 | +`"true"` before protection is applied — set by an external actor, another |
| 113 | +controller, or a manual operation — bootc-operator leaves it alone: |
| 114 | + |
| 115 | +- It does **not** set the corresponding tracking annotation on the `BootcNode`. |
| 116 | +- It does **not** remove the annotation when the slot is freed. |
| 117 | + |
| 118 | +If the annotation is present but set to any value other than `"true"` (for |
| 119 | +example `"false"`), bootc-operator overwrites it with `"true"` and records |
| 120 | +ownership. This is required because Karpenter only honors the literal value |
| 121 | +`"true"`; any other value leaves the node unprotected. |
| 122 | + |
| 123 | +The two annotations are tracked independently. If only one is already set to |
| 124 | +`"true"`, the operator owns (and later removes) only the other. |
| 125 | + |
| 126 | +## Temporary overprovisioning during node drains |
| 127 | + |
| 128 | +During a bootc-operator update, the following sequence can cause temporary |
| 129 | +overprovisioning: |
| 130 | + |
| 131 | +1. bootc-operator evicts workloads from the node being updated. |
| 132 | +2. The evicted workloads become pending. |
| 133 | +3. Karpenter provisions new capacity to satisfy the pending pods. |
| 134 | +4. The original node reboots into the new image and rejoins the cluster. |
| 135 | +5. The newly provisioned capacity may become excess and be consolidated later. |
| 136 | + |
| 137 | +This is expected behavior. The `do-not-disrupt` annotation suppresses |
| 138 | +Karpenter's voluntary disruption of the node being updated, but it does not |
| 139 | +suppress Karpenter provisioning new capacity for pending workloads. Karpenter |
| 140 | +provisioning is driven by pending pod demand rather than disruption eligibility. |
| 141 | + |
| 142 | +## Post-rollout consolidation and administrator policy |
| 143 | + |
| 144 | +bootc-operator's protection is intentionally limited to the update lifecycle. |
| 145 | +When a node is Ready and the new image has been verified, the operator frees its |
| 146 | +reboot slot and removes only the Karpenter annotations that it added. If the |
| 147 | +workload has moved to newly provisioned capacity and the original node is now |
| 148 | +empty, Karpenter may then consolidate that node with an `Empty` disruption. This |
| 149 | +is normal Karpenter behavior after the bootc update has completed; it is not an |
| 150 | +OS update failure. |
| 151 | + |
| 152 | +Cluster administrators who need a node to remain in service after the rollout |
| 153 | +must configure that steady-state policy in Karpenter. bootc-operator should not |
| 154 | +permanently disable consolidation by default, because doing so can leave unused |
| 155 | +capacity running indefinitely. |
| 156 | + |
| 157 | +This is an intentional separation of responsibilities. bootc-operator protects |
| 158 | +only the Karpenter-owned nodes that are participating in its update lifecycle; |
| 159 | +it does not apply protection to every node in the `BootcNodePool`. |
| 160 | + |
| 161 | +| Responsibility | bootc-operator | Cluster administrator / Karpenter | |
| 162 | +|---|---|---| |
| 163 | +| Protect a node during image staging, drain, reboot, and health verification | Applies temporary node and NodeClaim annotations and checks for conflicting Karpenter operations | — | |
| 164 | +| Provision capacity for pending workloads | — | Karpenter provisions nodes according to its NodePool and workload requirements | |
| 165 | +| Consolidate or drift unrelated nodes | — | Karpenter manages nodes according to the configured disruption policy | |
| 166 | +| Retain a node after the rollout becomes complete | Removes only annotations it owns; does not freeze unrelated nodes | Configure `do-not-disrupt`, disruption budgets, or `consolidateAfter` as needed | |
| 167 | +| Protect an entire Karpenter pool during maintenance | — | Configure an explicit Karpenter-wide maintenance policy | |
| 168 | +| Handle expiration, interruption, manual deletion, or other forceful termination | Detects some conflicts and defers its own update when possible | Configure and operate the applicable Karpenter and infrastructure policies | |
| 169 | + |
| 170 | +If an entire pool must be held stable, configure that as an explicit Karpenter |
| 171 | +maintenance policy rather than relying on bootc-operator to freeze unrelated |
| 172 | +nodes. |
| 173 | + |
| 174 | +Options include: |
| 175 | + |
| 176 | +1. **Keep a node protected explicitly.** Set |
| 177 | + `karpenter.sh/do-not-disrupt: "true"` on the Node and its NodeClaim. An |
| 178 | + annotation that was already present before bootc-operator starts protecting |
| 179 | + the node is treated as externally owned and is not removed when the rollout |
| 180 | + completes. This blocks voluntary disruption such as consolidation and drift, |
| 181 | + but not expiration, interruption, manual deletion, or other forceful |
| 182 | + termination. |
| 183 | +2. **Temporarily block disruption for the whole Karpenter NodePool.** During a |
| 184 | + rollout, an administrator can set a zero disruption budget: |
| 185 | + |
| 186 | + ```yaml |
| 187 | + spec: |
| 188 | + disruption: |
| 189 | + budgets: |
| 190 | + - nodes: "0" |
| 191 | + ``` |
| 192 | +
|
| 193 | + Restore the normal budget after the rollout. This protects all nodes in the |
| 194 | + pool, not just the node being updated. |
| 195 | +3. **Delay consolidation.** Configure a suitable `consolidateAfter` interval so |
| 196 | + that a rebooted node has time to become schedulable and receive workloads |
| 197 | + before Karpenter considers it empty. This reduces the race but does not |
| 198 | + guarantee that the node will be retained. |
| 199 | + |
| 200 | +All three options are administrator-owned policy. The first is appropriate for |
| 201 | +selected nodes that must remain stable; the second is appropriate when an |
| 202 | +entire pool must be held stable during a controlled maintenance window; and the |
| 203 | +third is appropriate when consolidation should remain enabled but be delayed. |
| 204 | + |
| 205 | +## OS initialization startup taint |
| 206 | + |
| 207 | +The lifecycle-manager-neutral behavior and taint lifecycle are documented in |
| 208 | +[OS initialization taint](os-not-ready-taint.md). With Karpenter, configure |
| 209 | +the taint as a `startupTaint` on the `NodePool`. |
| 210 | + |
| 211 | +Add `bootc.dev/os-not-ready` to the `startupTaints` list in the Karpenter |
| 212 | +`NodePool`: |
| 213 | + |
| 214 | +```yaml |
| 215 | +apiVersion: karpenter.sh/v1 |
| 216 | +kind: NodePool |
| 217 | +metadata: |
| 218 | + name: default |
| 219 | +spec: |
| 220 | + template: |
| 221 | + spec: |
| 222 | + startupTaints: |
| 223 | + - key: bootc.dev/os-not-ready |
| 224 | + effect: NoSchedule |
| 225 | +``` |
| 226 | + |
| 227 | +No changes to the `BootcNodePool` spec are required. bootc-operator activates |
| 228 | +the feature automatically when it detects the taint on a newly joined node. |
| 229 | + |
| 230 | +### `startupTaints` vs `taints` |
| 231 | + |
| 232 | +This taint **must** be declared under `startupTaints`, not `taints`, in the |
| 233 | +Karpenter `NodePool`. |
| 234 | + |
| 235 | +Karpenter uses `startupTaints` to mark nodes that require external |
| 236 | +initialization before workloads are scheduled. It factors the taint into its |
| 237 | +bin-packing decisions and does not treat nodes with the taint as unable to |
| 238 | +satisfy pending pods. |
| 239 | + |
| 240 | +If the taint is instead declared under `taints` (permanent taints), Karpenter |
| 241 | +will observe that pending pods cannot be scheduled on nodes carrying the taint |
| 242 | +and will provision additional nodes in a loop — each of which will also carry |
| 243 | +the taint, perpetuating the loop. |
| 244 | + |
| 245 | +## Limitations |
| 246 | + |
| 247 | +`karpenter.sh/do-not-disrupt` only blocks voluntary disruption methods |
| 248 | +(consolidation and drift). It does not protect a node against forceful |
| 249 | +termination triggered by expiration (`expireAfter`), interruption (spot |
| 250 | +instance reclaim), Node Auto Repair, or manual deletion. If any of those |
| 251 | +events occur while a node is in a bootc-operator reboot slot, Karpenter may |
| 252 | +still terminate it. |
| 253 | + |
| 254 | +`karpenter.sh/do-not-repair` similarly has no effect on voluntary disruption |
| 255 | +or expiration — it only suppresses Node Auto Repair. |
| 256 | + |
| 257 | +If a node is terminated by expiration, interruption, or any other forceful |
| 258 | +method while a bootc-operator update is in progress, the termination is caused |
| 259 | +by Karpenter acting on its own policy — not by the bootc-operator update. The |
| 260 | +update simply happened to be in flight at the time. |
| 261 | + |
| 262 | +## Residual race and coordination guarantees |
| 263 | + |
| 264 | +The annotations and disruption checks are not an authoritative mutex. Karpenter |
| 265 | +does not currently acknowledge `do-not-repair`, expose a shared Lease, or |
| 266 | +coordinate its reboot commit with bootc-operator's slot assignment. Karpenter |
| 267 | +can therefore commit a disruption immediately before or after bootc-operator's |
| 268 | +last check. Setting `do-not-repair` does not cancel a disruption that Karpenter |
| 269 | +has already committed. |
| 270 | + |
| 271 | +The integration narrows this window substantially by: |
| 272 | + |
| 273 | +- Applying protection from the start of image staging rather than only at |
| 274 | + slot assignment. |
| 275 | +- Checking all disruption signals (reboot taint, disrupted taint, deletion |
| 276 | + timestamp, and NodeClaim conditions) before both slot assignment and drain. |
| 277 | + |
| 278 | +Full serialization requires Karpenter support for a shared ownership protocol |
| 279 | +or an admission/provider mechanism that prevents a competing operation from |
| 280 | +being committed. |
| 281 | + |
| 282 | +Refer to the [Karpenter disruption documentation](https://karpenter.sh/docs/concepts/disruption/) |
| 283 | +for a full breakdown of which controls apply to which disruption methods. |
| 284 | + |
| 285 | +## Non-Karpenter nodes |
| 286 | + |
| 287 | +Nodes without a `NodeClaim` owner reference are unaffected. The feature |
| 288 | +activates only when the owner reference check matches. |
| 289 | + |
| 290 | +## Annotation reference |
| 291 | + |
| 292 | +| Annotation | Object | Managed by | Purpose | |
| 293 | +|---|---|---|---| |
| 294 | +| `karpenter.sh/do-not-repair` | K8s Node | bootc-operator (or external) | Prevents Karpenter Node Auto Repair from replacing this node during an update | |
| 295 | +| `karpenter.sh/do-not-disrupt` | K8s Node | bootc-operator (or external) | Prevents Karpenter voluntary disruption (consolidation, drift) during an update | |
| 296 | +| `karpenter.sh/do-not-repair` | NodeClaim | bootc-operator (or external) | Best-effort suppression of Karpenter repair selection before an operator reboot | |
| 297 | +| `karpenter.sh/do-not-disrupt` | NodeClaim | bootc-operator (or external) | Best-effort suppression of Karpenter voluntary disruption before an operator reboot | |
| 298 | +| `bootc.dev/owns-karpenter-do-not-repair` | BootcNode | bootc-operator | Tracks that the operator added `do-not-repair`; cleared when the annotation is removed on slot free | |
| 299 | +| `bootc.dev/owns-karpenter-do-not-disrupt` | BootcNode | bootc-operator | Tracks that the operator added `do-not-disrupt`; cleared when the annotation is removed on slot free | |
| 300 | +| `bootc.dev/owns-karpenter-nodeclaim-do-not-repair` | BootcNode | bootc-operator | Tracks ownership of NodeClaim `do-not-repair` | |
| 301 | +| `bootc.dev/owns-karpenter-nodeclaim-do-not-disrupt` | BootcNode | bootc-operator | Tracks ownership of NodeClaim `do-not-disrupt` | |
0 commit comments