Skip to content

fix(vm-instance)!: refuse resources that cannot size the VM - #4405

Merged
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/vm-instance-cpu-cores
Sep 23, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/vm-instance-cpu-cores

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

A vm-instance could ask for half a core and get one, with no error anywhere. resources.cpu and resources.sockets are typed as quantities, but they end up in domain.cpu.cores and domain.cpu.sockets, which KubeVirt declares as unsigned integers. The chart cast them with int64. That turns 500m or 1.5 into 0 without an error, and KubeVirt boots a zero core count as one core.

The chart now reads these two fields the way an integer field reads them. It decodes the raw value as YAML and stops the render unless the result is a positive whole number that fits in uint32. So 2, "2.0", "+2" and "1e1" render as the integer they decode to. 500m, 2000m, 1k, "1.5", 0, "0", "-2", "010" and "4294967296" stop the render with an error that names the field. A leading zero is refused because YAML reads 010 as octal 8. The check looks at presence, not truthiness, so an unquoted 0 is refused instead of being read as unset. It is the same rule #4386 uses for kubernetes-nodes.

Memory gets a presence check too, which covers the case from the issue thread. A quoted "0" was truthy and rendered requests.memory: "0", while an unquoted 0 was falsy and got dropped. Both spellings of zero, and any negative amount, now stop the render. Any other value passes through unchanged, because KubeVirt takes memory as a quantity. That includes an amount below one byte, such as 500m or 1e-3, which renders as it does on main today; this change does not refuse it.

The check lives in virtual-machine.domainResources. The VM spec, the instance type decision and the update hook's patch all take their values from that helper. The tests that pinned cores: 0 as the old behaviour now expect the refusal, and the helper comments about the truncation are trimmed.

This is a breaking change. A release that carries one of these values fails its next upgrade until the value is fixed: a cpu or sockets that is not a positive whole number within uint32 or has a leading zero, and a zero or negative memory. This includes a cpu or sockets set on its own next to an instanceType, which used to be ignored whatever it held.

Some values change meaning instead of failing. A value like "1e1" or "2." was truncated to 0 by the int64 cast, so the VM booted with one core. Now it gets the count the value spells, so that VM changes size on upgrade.

I refuse these values instead of rounding them, because silent coercion is how 500m became one core in the first place. The issue asks for a deprecation path, and I did not add a migration that rewrites stored values. It would have to guess what 500m meant, and both options (one core, or dropping the field) hide the same mistake again. The HelmRelease upgrade retries without uninstalling, so the running VM stays up until someone fixes the value. #4386 makes the same trade-off.

The check runs at render time, not at admission. The schema still accepts 500m, so the API takes the object and the error shows up in the HelmRelease status. Narrowing the schema to integers, as the issue also proposes, would move the error to admission, but it needs a migration of stored values. I left it for a separate change.

The field descriptions now say which values are accepted, and make generate carried them into the README, the schema, the Go types and the ApplicationDefinition.

The new tests/resources_values_test.yaml covers the accepted and refused spellings for cpu, sockets and memory. tests/instancetype_resources_test.yaml gets a case for a lone cpu next to cpuModel. The whole vm-instance suite passes. Every refusal case in the new file, and the "1e1" case, fails against the current main template.

Closes #4104

Screenshots

No UI change.

Downstream repositories

I walked the trigger map against the diff. No field is added, removed or renamed, and no default or enum changes. Only three field descriptions change, and the website reference page is regenerated from the README at release time.

Release note

fix(vm-instance)!: resources.cpu and resources.sockets must now be positive whole numbers, and resources.memory a positive quantity. A cpu or sockets that is not a plain whole number within uint32 (for example 500m, 1k or 1.5), or a zero or negative memory, now fails at render time with an error naming the field, instead of silently becoming a one-core VM or a zero memory request. A release carrying such a value fails its next upgrade until the value is fixed. A value such as 1e1 or 2., which used to be truncated to one core, now sizes the VM with the count it spells.

Summary by CodeRabbit

  • Bug Fixes

    • VM sizing now accepts only positive whole-number CPU and socket values within the supported range. Fractional, millicore, zero, negative, and out-of-range values are rejected during rendering.
    • Memory values must be positive quantities. A CPU or socket value specified alone does not size the VM and is ignored; incomplete sizing is rejected where required.
  • Documentation

    • Updated VM instance resource guidance to clarify CPU and socket requirements, memory validation, and how socket values define vCPU topology.

resources.cpu and resources.sockets are typed as quantities, but they
land in domain.cpu.cores and domain.cpu.sockets, which KubeVirt
declares as uint32. The chart cast them with int64, which turns 500m,
1.5 or 1e1 into 0 without an error, and KubeVirt boots a zero core
count as one core. The user asked for half a core and got a one-core
VM with nothing reporting it.

The raw spelling is now decoded as YAML, the way an integer field
reads it, and the render stops unless the result is a positive whole
number that fits in uint32. A leading zero is refused, because YAML
reads 010 as octal 8. The check keys on presence rather than
truthiness, so an unquoted 0 is refused instead of being read as
unset.

Memory gets the same presence check. A quoted "0" was truthy and
rendered requests.memory: "0", while an unquoted 0 was falsy and
dropped, so one quantity took two paths. A zero or negative amount
now stops the render in both spellings.

BREAKING CHANGE: values that used to render now either fail or
render differently. A cpu or sockets that does not decode to a plain
whole number within uint32 (a millicore, suffixed such as 1k,
fractional, zero, negative or leading-zero value), and a zero or
negative memory, fail the next upgrade of the release until the
value is fixed. This includes cpu or sockets set on its own beside an
instanceType, which used to be ignored whatever it held. A spelling
that the int64 cast truncated to 0 but that YAML decodes to a whole
number, such as the exponent 1e1 or the trailing dot 2., used to boot
one core and now sizes the VM with the count it spells, so such a VM
changes size on upgrade.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
@github-actions github-actions Bot added area/virtualization Issues or PRs related to virtualization (kubevirt, cdi, vmi, vm-import) kind/breaking-change Indicates the change introduces a breaking API or behaviour change kind/bug Categorizes issue or PR as related to a bug size/XL This PR changes 500-999 lines, ignoring generated files labels Sep 23, 2026
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: cozystack/cozystack/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 67529738-9451-4cf8-a620-50632ad8574c

📥 Commits

Reviewing files that changed from the base of the PR and between 89da4a1 and c290dee.

📒 Files selected for processing (9)
  • api/apps/v1alpha1/vminstance/types.go
  • packages/apps/vm-instance/README.md
  • packages/apps/vm-instance/templates/_helpers.tpl
  • packages/apps/vm-instance/tests/instancetype_resources_test.yaml
  • packages/apps/vm-instance/tests/nulled_resources_values.yaml
  • packages/apps/vm-instance/tests/resources_values_test.yaml
  • packages/apps/vm-instance/values.schema.json
  • packages/apps/vm-instance/values.yaml
  • packages/system/vm-instance-rd/cozyrds/vm-instance.yaml

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The VM-instance Helm template now validates CPU, socket, and memory resource values before rendering VM sizing. Tests cover accepted and rejected values, including resources used with instance types. API, chart, and VM-instance documentation describe the validation requirements.

Changes

VM resource validation

Layer / File(s) Summary
Validate and document VM resources
api/apps/v1alpha1/vminstance/types.go, packages/apps/vm-instance/templates/_helpers.tpl, packages/apps/vm-instance/tests/*.yaml, packages/apps/vm-instance/README.md, packages/apps/vm-instance/values.*, packages/system/vm-instance-rd/cozyrds/vm-instance.yaml
The template validates CPU and sockets as positive whole numbers within uint32 range, and memory as a positive quantity. Tests cover valid values, invalid quantities, and instance-type cases. The API and chart documentation describe these constraints.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to c290d

The resource validation and its documented constraints are ready to merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning [#4104] The helper validates resources.cpu and resources.sockets as positive whole numbers within uint32, rejects fractional and sub-unit values at render time, and includes tests for rejection … Add a deprecation or migration path for existing invalid resources.cpu and resources.sockets values before enforcing render-time rejection.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The helper changes, validation tests, and updated API, chart, and README descriptions all support the #4104 CPU and socket validation objective. The additional positive-memory validation and its tests…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 1…
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: reject resource values that cannot size the VM.
Full details: Linked Issues check

Explanation

[#4104] The helper validates resources.cpu and resources.sockets as positive whole numbers within uint32, rejects fractional and sub-unit values at render time, and includes tests for rejection and accepted spellings. This prevents silent truncation and selects rejection over rounding. However, #4104 says existing quantity values need a deprecation path. The PR instead makes affected existing values fail rendering immediately; the supplied summary confirms that upgrades fail until users correct them.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

LGTM with non-blocking notes

This goes after the root cause rather than the symptom, and refusing a value beats rounding it into something that looks set. The breaking half is declared instead of smuggled in. What's left is smaller: one missing comparison, and two sentences that undersell what the guard already does.

Findings

  • [MINOR] packages/apps/vm-instance/templates/_helpers.tpl:106, the memory guard has no floor, and KubeVirt puts one at 1M
  • [MINOR] packages/apps/vm-instance/values.yaml:23, the cpu and sockets descriptions name two of the five refused shapes
  • [MINOR] packages/apps/vm-instance/templates/_helpers.tpl:93, the error quotes a number the operator never wrote

Notes

  • "It is the same rule #4386 uses for kubernetes-nodes" is not quite it yet: #4386 is still open and its $whole has no upper bound, while this one caps at 4294967295. Both counts land in a uint32, so until #4386 picks the cap up the two charts accept different sets.
  • Nothing under hack/e2e-chainsaw/ sets resources at all (vminstance.yaml sizes by instanceType), so the coverage here is helm-unittest only.
  • That a failed upgrade leaves the VM running is read off pkg/registry/apps/application/rest.go:1608 and the nil-remediation assertions in rest_helmrelease_spec_test.go:107,126, not replayed on a cluster. Worth one manual pass on a disposable cluster with an existing VMInstance carrying cpu: 500m before this ships, to confirm the HelmRelease message actually reaches the tenant.
  • Corners with a real instanceType cannot be rendered outside the unittest harness, so the "a whole cpu on its own beside an instance type is ignored" half rests on the suite.

{{- $memory := index $resources "memory" -}}
{{- if not (kindIs "invalid" $memory) -}}
{{- $rawMemory := printf "%v" $memory -}}
{{- if not (gt (regexFind "^[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)" $rawMemory | float64) 0.0) -}}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] the memory guard has no floor, and KubeVirt puts one at 1M

regexFind "^[+-]?(...)" takes the leading numeric prefix, so the suffix never enters the comparison and the test is only "greater than zero". Rendered at this head, memory of 500m, 1n, 1m, 0.0001Gi, 1e-3 and 0.5 all pass through unchanged; only 0, "0" and a negative are refused.

KubeVirt refuses the same values a step later. At v1.9.0, the tag pinned by packages/system/kubevirt-operator/templates/kubevirt-operator.yaml:8685, pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter.go:1135 reads:

	} else if spec.Domain.Resources.Requests.Memory().Value() > 0 && spec.Domain.Resources.Requests.Memory().Cmp(resource.MustParse("1M")) < 0 {
		causes = append(causes, metav1.StatusCause{
			Type: metav1.CauseTypeFieldValueInvalid,
			Message: fmt.Sprintf("%s '%s': must be greater than or equal to 1M.", field.Child("domain", "resources", "requests", "memory").String(),
				spec.Domain.Resources.Requests.Memory()),

Value() rounds a sub-unit quantity up to 1, so memory: 500m lands squarely in that window. The result is the shape this PR exists to close, one field over: the render succeeds, admission rejects the object, and the message names spec.template.spec.domain.resources.requests.memory rather than the field the user set. The PR puts this out of scope and it is unchanged from main, so it does not block, but the fix is the comparison you already have: require at least 1M instead of more than zero.

## @field {quantity} [cpu] - Number of CPU cores allocated.
## @field {quantity} [memory] - Amount of memory allocated.
## @field {quantity} [sockets] - Number of CPU sockets (vCPU topology).
## @field {quantity} [cpu] - Number of CPU cores allocated, as a positive whole number such as `2`. Millicore and fractional quantities are rejected at render time, because the value becomes the VM's integer `domain.cpu.cores`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] the cpu and sockets descriptions name two of the five refused shapes

"Millicore and fractional quantities are rejected at render time" leaves out a whole count carried by a suffix, a value past uint32, zero and negative, and a leading zero, each of which the new suite has a case for. Rendered at this head, resources.cpu: "1k" fails with resources.cpu (1k) must be a positive whole number, such as 2.

cozyvalues-gen copies this prose verbatim into values.schema.json, README.md and the -rd openAPISchema, which is what the dashboard shows, so an operator reading the field is told 1k is fine. Naming the rule beats naming two ways to break it: "a plain whole number from 1 to 4294967295, no suffix, no leading zero".

{{- range $field := list "cpu" "sockets" -}}
{{- $value := index $resources $field -}}
{{- if not (kindIs "invalid" $value) -}}
{{- $raw := printf "%v" $value -}}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] the error quotes a number the operator never wrote

$raw is printf "%v" of the value, and a numeric literal reaches the template as a Go float64, so %v prints it in %g form. A VMInstance carrying resources.cpu: 4294967296 fails with resources.cpu (4.294967296e+09) must be a positive whole number, and grepping the CR for that string finds nothing. printf "%.0f" for an integral count, %v for the rest.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 2b5770c into main Sep 23, 2026
50 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the fix/vm-instance-cpu-cores branch September 23, 2026 13:58
IvanHunters added a commit that referenced this pull request Oct 2, 2026
…ount (#4386)

## What this PR does

A worker pool's CPU value could land in the VM as something KubeVirt
cannot accept. `resources.cpu` is typed as a quantity, but on the
kubevirt substrate the template wrote it unchanged into
`domain.cpu.cores`, which is `uint32` in KubeVirt and an integer in the
KubevirtMachineTemplate CRD. With `resources.cpu: 500m` the chart
rendered `cores: 500m`. That string was truthy, so it also counted as
explicit sizing and dropped the pool's `instanceType` from the VM, and
then the API server refused the template anyway.

The render now decodes the value as YAML, the same way `cores: <raw>` is
decoded, and stops unless the result is a whole number from 1 to
4294967295. `500m`, `"1.5"` and `"1e10"` now fail at render time with an
error naming the field, before the value reaches the VM template or the
instancetype decision. `"0"` and `"-2"` were already refused on `main`
and still are.

Only inputs that could not produce a working pool are refused. Every
value that rendered a pool before renders the same bytes now, so the
content-hashed template keeps its name and nothing rolls. That includes
`2`, `"2.0"`, `"+2"` and `"1e1"`, and also two odd cases I kept on
purpose because pools may already carry them. An unquoted `0` is falsy,
so the pool renders without `domain.cpu` and is sized by its
instancetype, as on `main`. A leading-zero value such as `"010"` is
still emitted as written and keeps the reading it gets on `main` today,
where YAML treats it as octal. I checked both against a render of
`main`, and the tests pin the template names that `main` produces for
them.

This is where kubernetes-nodes and vm-instance now differ. Since #4405,
vm-instance refuses a leading zero and an unquoted `0`, and this chart
does not. Stored worker pools can carry these values, because migration
56 copies `resources` verbatim, so refusing them here without a
migration would fail those pools' releases on their next upgrade. #4554
tracks the migration and the refusal after it.

The proxmox substrate is not checked. It converts the quantity to
millicores and rounds up to whole cores, so `500m` is a valid size there
and becomes one core. The field description now says what the value
means on each substrate.

Memory is left alone. It goes to `domain.memory.guest`, which is a
`resource.Quantity`, so a quantity string is valid there.

The issue suggested coercing to an integer, as vm-instance did before
#4405. I didn't, because `int64` turns `500m` into `0` without an error.

The new helm-unittest suite covers each input form, both preserved
cases, the upper bound, the instancetype case and the proxmox path.

Two neighbouring descriptions still say only "CPU available.":
`nodeGroups[].resources.cpu` in `packages/extra/computeplane` and in the
parent `kubernetes` chart. computeplane passes `resources` straight into
this chart, so the new check applies to its pools as well. I left both
alone to keep this change to one chart.

Closes #4150

### Screenshots

No UI change.

### Downstream repositories

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
- [ ] [cozystack/community](https://github.com/cozystack/community) -
follow-up:

### Release note

```release-note
fix(kubernetes-nodes): on the kubevirt substrate, a worker pool whose resources.cpu cannot be a whole vCPU core count (for example 500m, 1.5 or a value above 4294967295) now fails at render time with a clear error, instead of rendering a VM template that is refused later or dropping the pool's instanceType. Values that rendered a working pool before render unchanged.
```



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* KubeVirt CPU settings now accept whole-core values from 1 to
4,294,967,295. Fractional, millicore, negative, and larger values are
rejected; an unquoted `0` is treated as unset.
* Proxmox rounds fractional and millicore CPU values up to whole cores.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/virtualization Issues or PRs related to virtualization (kubevirt, cdi, vmi, vm-import) kind/breaking-change Indicates the change introduces a breaking API or behaviour change kind/bug Categorizes issue or PR as related to a bug size/XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

vm-instance: resources.cpu and resources.sockets accept quantity strings for integer-only fields, and a fractional value silently becomes one core

2 participants