Skip to content

fix(resources-management): parse fractional CPU and memory quantities - #217

Merged
Suselz merged 2 commits into
mainfrom
fix-fractional-quantity-conversion
Sep 18, 2026
Merged

Suselz merged 2 commits into
mainfrom
fix-fractional-quantity-conversion

Conversation

@Suselz

@Suselz Suselz commented Sep 18, 2026

Copy link
Copy Markdown
Member

What is broken

helm_lib_resources_management_cpu_units_to_millicores and
helm_lib_resources_management_memory_units_to_bytes convert a quantity with atoi, which is
strconv.Atoi with the error discarded. It parses integers only, so every fractional quantity
becomes 0 — silently, without an error:

cpu "0.5"   -> 0
cpu "0.5m"  -> 0.5      (not an integer number of millicores)
cpu "500m"  -> 500
cpu "1"     -> 1000
mem "0.5Gi" -> 0

Kubernetes accepts a decimal mantissa, so 0.5 and 500m denote the same amount of CPU, and
0.5Gi is a valid amount of memory.

Why it reaches users

helm_lib_resources_management_original_pod_resources copies requests from the raw value while
computing limits through the conversion, so the two disagree. With vpa.cpu.min set to a
fractional value and a limitRatio:

limits:
  cpu: 0m
  memory: "0"
requests:
  cpu: "0.5"
  memory: 0.5Gi

The API server rejects such a pod: requests must not exceed limits.

The schemas of the consuming modules let these values through. In istio and cni-cilium the
memory pattern is ^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$, which permits a decimal
part outright. The CPU pattern ^[0-9]+m?$ rejects the quoted form "0.5", but the schema also
allows type: number, so an unquoted 0.5 passes validation and then breaks.

The reason this has gone unnoticed is that the affected branch is opt-in: no limits are computed
unless limitRatio is set, and it has no default. Every documented example uses m and Mi
notation.

What this pull request changes

Both helpers now resolve the mantissa and the multiplier first, then convert:

  • an integer mantissa keeps exact integer arithmetic, so no value that works today changes its
    result, including the large binary suffixes where float arithmetic would lose precision;
  • a fractional mantissa goes through float arithmetic and is rounded up, so a limit is never
    smaller than the value asked for.

The CPU helper also gained the validation that the memory helper already had. An unparsable value
now fails the rendering instead of silently yielding 0.

Please weigh that last point: it is a behaviour change. A value that reaches this branch already
produced limits.cpu: 0m, which the API server rejects, so it cannot be a working configuration
today. Failing loudly seemed better than keeping a silent zero, but say so if you would rather keep
the helpers permissive and fix only the parsing.

Verification

helm unittest ./tests — 411 tests pass, including the existing snapshot. The two helper suites
gained cases for fractional values, a leading-dot value, an unquoted YAML number, and rounding up
of a fractional millicore.

Rendering helm_lib_resources_management_original_pod_resources with the same input as above now
gives a valid object, and the whole-unit case is byte-for-byte unchanged:

unquoted_number_cpu_min:
  limits:
    cpu: 750m
    memory: "402653184"
  requests:
    cpu: "0.5"
    memory: 256Mi
millicores_unchanged:
  limits:
    cpu: 75m
    memory: "402653184"
  requests:
    cpu: 50m
    memory: 256Mi

Release

The chart version is bumped to 1.72.23. The platform picks the fix up through
make update-lib-helm version=1.72.23, which also moves LIB_HELM_VERSION in its Makefile,
currently pinned at 1.72.21.

Context

Found while fixing the same defect in the user-authn module of the platform, where a
DexAuthenticator with cpu: "0.5" rendered an init container with limits.cpu: 0m against
requests.cpu: 10m and stopped the main queue:
deckhouse/deckhouse#23170.

charts/helm_lib/README.md is regenerated by go run tools/build-doc.go.

`helm_lib_resources_management_cpu_units_to_millicores` and
`helm_lib_resources_management_memory_units_to_bytes` converted a quantity
with `atoi`, which parses integers only and returns 0 on failure. Every
fractional quantity therefore became 0, silently and without an error.

Kubernetes accepts a decimal mantissa, so `0.5` and `500m` denote the same
amount of CPU, and `0.5Gi` is a valid amount of memory. The schemas of the
modules that consume these helpers accept such values as well: the memory
pattern permits a decimal part outright, and a CPU value passes as a plain
YAML number.

The damage is worst in `helm_lib_resources_management_original_pod_resources`,
which copies requests from the raw value while computing limits through the
conversion. With `vpa.cpu.min: 0.5` and a `limitRatio` it produced
`requests.cpu: 0.5` against `limits.cpu: 0m`, and the API server rejects such
a pod.

Both helpers now keep exact integer arithmetic for an integer mantissa and
switch to float arithmetic only for a fractional one, so no existing value
changes its result. A fractional result is rounded up, so a limit is never
smaller than the value asked for.

The CPU helper also gained the validation the memory helper already had: an
unparsable value now fails the rendering instead of silently yielding 0.
A value that reaches this branch already produced a limit of `0m`, so nothing
that worked before stops working.

Signed-off-by: suselz <suselz@mail.ru>
Suselz added a commit to deckhouse/deckhouse that referenced this pull request Sep 18, 2026
The previous commit carried a local copy of the CPU and memory conversion,
because the helm_lib helpers cannot parse a fractional quantity. That defect
belongs to the library and is fixed there in deckhouse/lib-helm#217, so the
module calls the helpers again and this pull request keeps only what is
specific to it.

What remains is the lower bound. The limits of the `self-signed-generator`
init container are the sum of the main container limits, and the requests of
that container are hardcoded as 10m and 10Mi. A DexAuthenticator whose limits
sum to less than that produced a Deployment with a limit below its own
request, which the API server rejects. The sum is now never lower than the
requests.

Staying under the sum is safe: a ResourceQuota counts a pod as
max(init, sum(app)), so the accounting is still driven by the main containers.

Fixing a fractional quantity such as `cpu: "0.5"` needs the library release
and a bump of LIB_HELM_VERSION, and is not part of this pull request.

Signed-off-by: suselz <suselz@mail.ru>
@Suselz Suselz self-assigned this Sep 18, 2026
Signed-off-by: suselz <suselz@mail.ru>
@Suselz
Suselz marked this pull request as ready for review September 18, 2026 21:27
@Suselz
Suselz merged commit 82ec0f0 into main Sep 18, 2026
4 checks passed
Suselz added a commit to deckhouse/deckhouse that referenced this pull request Sep 18, 2026
deckhouse/lib-helm#217 taught `helm_lib_resources_management_cpu_units_to_millicores`
and `helm_lib_resources_management_memory_units_to_bytes` to parse a fractional
quantity, which they previously converted to 0 through `atoi`. The module calls
those helpers when it sums the main container limits into the limits of the
`self-signed-generator` init container, so a DexAuthenticator with `cpu: "0.5"`
rendered `limits.cpu: 0m` against `requests.cpu: 10m` and the API server rejected
the Deployment.

The bump also carries deckhouse/lib-helm#216, which renders an extra
ClusterRoleBinding from `helm_lib_module_controller_rbac`. No module in this
repository uses that helper, so nothing here changes because of it.

The test for a fractional quantity is restored now that the conversion works.

Signed-off-by: suselz <suselz@mail.ru>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants