Skip to content

Commit ecdbdbd

Browse files
committed
fix(kubernetes-nodes): refuse a resources.cpu that cannot be a core count
On the kubevirt substrate a worker pool's resources.cpu, although schema-typed as a Kubernetes quantity, is written verbatim into domain.cpu.cores, which KubeVirt declares as uint32 and the KubevirtMachineTemplate CRD as an integer. A value such as 500m or "1.5" therefore rendered a template the API server refuses, and because the same raw string was truthy it also counted as explicit sizing and dropped the pool's instancetype matcher first. A whole count past the uint32 ceiling rendered too, although KubeVirt cannot hold it. The render now decodes the value the way the emitted cores field is decoded and stops unless that is a whole number between 1 and the uint32 maximum, before the value reaches the VM template or the precedence decision. Only inputs that could not produce a working pool are refused. Every value that rendered one still renders the same bytes, so no pool rolls: a bare numeric zero stays falsy and means unset, and a leading-zero spelling is still emitted as written. The proxmox substrate is left out of the check, because it converts the quantity to millicores and rounds up to whole cores, so 500m is a valid size there. The field description now says what the value means on each substrate. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
1 parent 88b61c9 commit ecdbdbd

7 files changed

Lines changed: 323 additions & 5 deletions

File tree

‎api/apps/v1alpha1/kubernetesnodes/types.go‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/apps/kubernetes-nodes/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ the parent cluster `kubernetes-<cluster>` in the same namespace, plus a
3737
| `diskSize` | System disk size for the worker VM. Carries the Talos OS image, kubelet state, containerd image cache, and any local-path PVCs. How the OS image reaches the disk follows this pool's `osImage`: the default and `osImage.factory` stream the Image Factory raw artifact in over HTTP, while `osImage.builtin` makes the disk a storage-layer CDI clone of a shared golden image, with no HTTP fetch. A `builtin` pool must keep diskSize at or above the golden's storage size (6Gi in the shipped worker image catalog), because a CDI clone target cannot be smaller than its source -- the chart rejects a smaller value at render time rather than letting the clone stall. Sizing a `builtin` pool also has to allow for transient headroom the finished disk does not show: CDI populates a larger target by cloning the golden at the source size and then growing it, so a worker being provisioned occupies a temporary clone PVC at the golden's size on top of this diskSize, and both are multiplied by the replica count of a replicated StorageClass. The golden itself is a separate one-time cost for the whole cluster, not a per-worker one. So the peak is the golden, plus `diskSize + golden storage` for each worker cloning at that moment. A pool whose storage backend cannot spare that peak does not fail: the clone waits for space, the VM never boots, and the reason is visible only in the storage layer's own events. | `quantity` | `20Gi` |
3838
| `roles` | List of node roles. Each role `r` labels nodes with `node-role.kubernetes.io/<r>`. Use `[ingress-nginx]` for a pool that hosts the tenant ingress-nginx controller. | `[]string` | `[]` |
3939
| `resources` | Explicit CPU and memory for each worker node, as an alternative to `instanceType` sizing. Optional: when omitted, the node is sized by `instanceType`. When both `cpu` and `memory` are set, they take precedence and `instanceType` is ignored (the instancetype is omitted from the VM, since KubeVirt cannot override an instancetype's CPU/memory). Set both `cpu` and `memory` together or neither; setting only one is rejected at render time. | `object` | `{}` |
40-
| `resources.cpu` | CPU available. | `quantity` | `""` |
40+
| `resources.cpu` | Number of vCPU cores for each worker VM, as a whole number such as `2`. On the `kubevirt` substrate the value becomes the VM's integer `domain.cpu.cores`, so millicore, fractional and negative quantities, and counts above 4294967295, are rejected at render time; an unquoted `0` is treated as unset. On the `proxmox` substrate a millicore or fractional quantity is rounded up to whole cores. | `quantity` | `""` |
4141
| `resources.memory` | Memory (RAM) available. | `quantity` | `""` |
4242
| `podCpuLimit` | CPU limit for the worker VM's virt-launcher `compute` container, which is the ceiling the guest vCPU threads, the QEMU emulator thread and the IO threads share. Distinct from `resources.cpu`, which sizes the guest itself. Cannot be combined with `instanceType` sizing: KubeVirt refuses a VM that references an instancetype and also sets `domain.resources.limits.cpu`, so the chart rejects that combination at render time. Sizing the pool by `resources` (both `cpu` and `memory`) is what drops the instancetype from the VM, which is where the limit is usable. When unset, KubeVirt derives the limit from the guest vCPU count in any namespace carrying a ResourceQuota that limits CPU, so a worker sized at N vCPUs runs under a ceiling of N with the emulation overhead taken out of the same N. Setting a value declares the limit on the VM directly, which is both what lands it on the container and what stops KubeVirt deriving one. It moves the CPU request with it: KubeVirt copies a declared CPU limit into the CPU request when the VM declares no request of its own, so a pool that sets only this field asks the scheduler for its whole ceiling instead of the fraction of the vCPU count it asked for before, and a ceiling the nodes cannot spare leaves the workers Pending on `Insufficient cpu`. Pair it with `podCpuRequest` to move the ceiling without moving the request. Zero means no ceiling at all rather than the tightest one, since the kubelet turns a zero CPU limit into no CFS quota while the declared key still stops KubeVirt deriving one — but it is only accepted together with `podCpuRequest: 0`, because KubeVirt keeps deriving a request from the vCPU count when the VM declares none and a request above a declared limit of zero is rejected when the worker's Pod is created. A negative value is refused at render time. Changing it rolls the pool, because the worker VM template is named by a hash of its content. Keep in sync with the parent kubernetes chart. | `quantity` | `""` |
4343
| `podCpuRequest` | CPU request for the worker VM's virt-launcher `compute` container, which is what the scheduler reserves for a worker on its host and what weights it against neighbours on a contended one. Distinct from `resources.cpu`, which sizes the guest itself. It exists to be paired with `podCpuLimit`: KubeVirt copies a declared CPU limit into the request when the VM declares no request, so raising the ceiling alone raises what every worker in the pool asks the scheduler for, and this field is what holds the request still while the ceiling moves. Cannot be combined with `instanceType` sizing, for the same reason as `podCpuLimit`: KubeVirt refuses a VM that references an instancetype and also sets `domain.resources.requests.cpu`, so the chart rejects that combination at render time, as it does a value above `podCpuLimit`. Usable without a limit, where it only changes the scheduler's weighting: KubeVirt still derives the ceiling from the guest vCPU count in any namespace carrying a ResourceQuota that limits CPU, so keep the request at or below that count. A request above that derived ceiling is refused only when the worker's Pod is created, and deliberately not at render time: the ceiling exists only where such a ResourceQuota does, so refusing here would take away a configuration that works in a namespace without one. Zero is accepted on its own and reserves nothing, but zero beside a `podCpuLimit` is refused, because KubeVirt cannot tell an explicit zero request from an absent one and replaces both with the limit — which is the outcome this field exists to avoid, arrived at silently. A negative value is refused as well. When unset, KubeVirt computes the request from the guest vCPU count and the cluster CPU allocation ratio. Changing it rolls the pool, because the worker VM template is named by a hash of its content. Keep in sync with the parent kubernetes chart. | `quantity` | `""` |

‎packages/apps/kubernetes-nodes/templates/nodegroup.yaml‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,28 @@
9292
{{- fail "podCpuLimit/podCpuRequest size the virt-launcher pod and are kubevirt-only" }}
9393
{{- end }}
9494
{{- end }}
95+
{{- /* On kubevirt, resources.cpu is emitted verbatim into domain.cpu.cores,
96+
an integer in the KubevirtMachineTemplate CRD and a uint32 in
97+
KubeVirt, and the same raw value decides below whether the
98+
instancetype matcher is dropped. A quantity such as 500m counts as
99+
sizing there and is then refused by the CRD, so a value that cannot
100+
be a core count stops here. Proxmox needs no such check: it converts
101+
the quantity to millicores and rounds up to whole cores.
102+
103+
The value is decoded as YAML, the way `cores: <raw>` is, so +2, 2.0
104+
or 1e1 pass as the integers they decode to, and a leading-zero
105+
spelling passes as whatever YAML reads it as. A bare numeric zero
106+
is left alone: it is falsy, so the pool renders without domain.cpu
107+
and is sized by its instancetype. */}}
108+
{{- $cpuValue := (.Values.resources | default dict).cpu }}
109+
{{- if and (not $isProxmox) (not (kindIs "invalid" $cpuValue)) (not (and (not (kindIs "string" $cpuValue)) (eq (float64 $cpuValue) 0.0))) }}
110+
{{- $cpu := printf "%v" $cpuValue }}
111+
{{- $cores := (printf "v: %s" $cpu | fromYaml).v }}
112+
{{- $isNumber := or (kindIs "int" $cores) (kindIs "int64" $cores) (kindIs "float64" $cores) }}
113+
{{- if not (and $isNumber (gt (float64 $cores) 0.0) (lt (float64 $cores) 4294967296.0) (eq (floor $cores) (float64 $cores))) }}
114+
{{- fail (printf "nodeGroup %s: resources.cpu (%s) must be a whole number of vCPU cores between 1 and 4294967295, such as 2. On the kubevirt substrate it becomes the worker VM's domain.cpu.cores, which KubeVirt declares as an unsigned 32-bit integer, so a millicore or fractional quantity cannot size the guest. Set a core count, or remove the field." $groupName $cpu) }}
115+
{{- end }}
116+
{{- end }}
95117
{{- $group := dict
96118
"minReplicas" .Values.minReplicas
97119
"maxReplicas" .Values.maxReplicas

0 commit comments

Comments
 (0)