feat(kubernetes): add kubelet resource reservations for worker nodes - #2420
Conversation
There was a problem hiding this comment.
Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.
Once credits are available, reopen this pull request to trigger a review.
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses recurring node stability issues in tenant Kubernetes clusters caused by memory pressure. By configuring kubelet with strict resource reservations and hard cgroup enforcement, the changes ensure that system and kubelet processes are protected from being terminated by the Linux OOM killer. While this introduces a minor reduction in total allocatable pod capacity, it provides a robust mechanism for maintaining node health and graceful pod eviction. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughPer-node-group Helm templates now compute effective CPU/memory (resolving Changes
Sequence Diagram(s)sequenceDiagram
participant Helm as "Helm template"
participant Lookup as "InstanceType lookup"
participant Compute as "Reservation calculator"
participant Renderer as "KubeadmConfigTemplate"
participant K8s as "MachineDeployment / Cluster Autoscaler"
Helm->>Lookup: resolve instanceType per nodeGroup
Lookup-->>Helm: instanceType spec (cpu/memory)
Helm->>Compute: compute effective CPU/memory, derive kubelet reservations & evictions
Compute-->>Helm: reservations, eviction args, validations
Helm->>Renderer: render KubeadmConfigTemplate with kubeletExtraArgs
Renderer-->>K8s: emit MachineDeployment, capacity annotations
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces resource reservation and eviction configurations to the Kubernetes cluster template. However, several critical issues were identified: the kubeletExtraArgs are missing required cgroup path flags for node allocatable enforcement, and several flags included are deprecated or removed in recent Kubernetes versions. Additionally, it is recommended to make the hardcoded reservation values configurable via values.yaml to support smaller node types. Finally, the PR description must be updated to include the mandatory release-note block as per the repository style guide.
| enforce-node-allocatable: "pods,system-reserved,kube-reserved" | ||
| cgroups-per-qos: "true" | ||
| cgroup-driver: "systemd" | ||
| kernel-memcg-notification: "true" |
There was a problem hiding this comment.
There are several issues in this block:
- Missing Cgroup Paths: When
enforce-node-allocatableincludessystem-reservedandkube-reserved, the kubelet requires the--system-reserved-cgroupand--kube-reserved-cgroupflags to be specified. Without them, the kubelet will fail to start. - Removed Flags: The flags
cgroups-per-qos,cgroup-driver, andkernel-memcg-notificationwere removed in Kubernetes 1.31 (deprecated in 1.28). Since this package defaults to v1.35, these flags will cause the kubelet to fail to start with an 'unknown flag' error. These settings should be managed viaKubeletConfiguration(which Kamaji already handles for the cgroup driver on line 143).
enforce-node-allocatable: "pods,system-reserved,kube-reserved"
system-reserved-cgroup: "/system.slice"
kube-reserved-cgroup: "/kubelet.slice"There was a problem hiding this comment.
All three deprecated flags (--cgroup-driver, --cgroups-per-qos, --kernel-memcg-notification) were removed in 390bdc4. enforce-node-allocatable is now set to pods only, so no cgroup path flags are needed.
| system-reserved: "cpu=100m,memory=512Mi" | ||
| kube-reserved: "cpu=100m,memory=512Mi" |
There was a problem hiding this comment.
These resource reservation values are hardcoded in the template. For smaller node types (e.g., u1.small with 2Gi RAM), a 1Gi reservation (512Mi + 512Mi) consumes 50% of the available memory, which might be excessive. Consider making these values configurable via values.yaml to allow users to tune them based on their node sizes.
There was a problem hiding this comment.
Reservation values are now configurable per node group via .nodeGroups[].kubelet fields. When left empty, they are auto-computed as 5% of effective capacity, clamped to [256Mi, 1Gi] for memory and [50m, 500m] for CPU.
| joinConfiguration: | ||
| nodeRegistration: | ||
| kubeletExtraArgs: {} | ||
| kubeletExtraArgs: |
There was a problem hiding this comment.
The pull request description is missing the required release-note block. According to the repository style guide (lines 51-57), every PR must contain a human-readable changelog entry in a specific format.
References
- PR body must contain a release-note block with type(scope): human-readable changelog entry. (link)
There was a problem hiding this comment.
The release-note block is present in the PR description.
85ccadb to
3d8147d
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/apps/kubernetes/templates/cluster.yaml`:
- Line 256: The hard eviction threshold "eviction-hard:
\"memory.available<512Mi\"" is not being included in pod allocatable/overhead
calculations; update the allocatable sizing computation to subtract this 512Mi
eviction reservation along with the existing "system-reserved" and
"kube-reserved" 512Mi entries so total reserved memory = 1536Mi (1.5Gi), and
update any related comments/variables (e.g., allocatable sizing, overhead
calculation, or capacity comments) to reflect that "eviction-hard" is a hard
reservation not available to pods.
- Line 260: The template enables enforce-node-allocatable for "system-reserved"
and "kube-reserved" but does not define their cgroup targets; update the same
kubelet config block in cluster.yaml to add system-reserved-cgroup and
kube-reserved-cgroup entries (matching the node image's systemd slices, e.g. the
slice used for kubelet and the container runtime) so kubelet can place reserved
resources into existing cgroups, or revert enforce-node-allocatable to only
"pods" until you validate the node cgroup layout; locate the block that contains
enforce-node-allocatable to add these keys and ensure the values reflect the
actual slices used on nodes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 528adf96-e476-40cd-8d80-a0d7888eeb3e
📒 Files selected for processing (1)
packages/apps/kubernetes/templates/cluster.yaml
3d8147d to
51ae51f
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/apps/kubernetes/values.schema.json (1)
27-36:⚠️ Potential issue | 🟠 MajorBreaking change:
kubeletis now required on every node group.Adding
kubelettoadditionalProperties.requiredmeans any existing tenantKubernetesresource with user-defined node groups beyondmd0(which only getskubelet: {}via the defaults for that specific key) will fail schema validation unless each node group explicitly sets akubeletfield. SinceKubeletinapi/apps/v1alpha1/kubernetes/types.gohas no+kubebuilder:default:={}, the API server will not auto-populate it either.Consider either (a) dropping
kubeletfrom the per-node-grouprequiredlist and treating it as optional (the Helm template can fall back to defaults when absent), or (b) adding a+kubebuilder:default:={}marker onNodeGroup.Kubeletintypes.goand regenerating, so existing configs continue to apply.Note: this file is regenerated by
cozyvalues-gen; fix the upstreamvalues.yaml/types.gorather than editing the schema directly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/apps/kubernetes/values.schema.json` around lines 27 - 36, The schema change made `kubelet` mandatory by adding it to additionalProperties.required in values.schema.json which will break existing tenant Kubernetes resources; either remove "kubelet" from the per-node-group required list in the source values.yaml used by cozyvalues-gen so node groups remain optional, or add a kubebuilder default on the NodeGroup.Kubelet field (add +kubebuilder:default:={} to NodeGroup.Kubelet in api/apps/v1alpha1/kubernetes/types.go) and re-run cozyvalues-gen to regenerate values.schema.json so existing configs are auto-populated. Ensure you pick one approach (prefer fixing types.go and regenerating) and do not edit the generated JSON directly.packages/system/kubernetes-rd/cozyrds/kubernetes.yaml (1)
11-29:⚠️ Potential issue | 🟠 MajorPre-commit
make generatemodified this file — regenerate and commit.The pre-commit hook reports this file was rewritten by
make generate. Since the embeddedopenAPISchemaandkeysOrderare derived from the chart'svalues.yaml/values.schema.json, please rerunmake generatelocally and commit the resulting diff so this file is consistent with the other generated artifacts in the PR.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/system/kubernetes-rd/cozyrds/kubernetes.yaml` around lines 11 - 29, The generated Kubernetes manifest’s embedded openAPISchema and keysOrder were changed by pre-commit; run the repository generator and commit the regenerated output: run make generate locally to update the openAPISchema (the large Chart Values JSON object) and the keysOrder array, verify the kubernetes.yaml changes (the Chart Values / keysOrder blocks shown in the diff), then add and commit the regenerated file so the pre-commit hook no longer rewrites kubernetes.yaml.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@api/apps/v1alpha1/kubernetes/types.go`:
- Around line 190-207: The doc comment for the Kubelet struct's
EvictionHardMemory field is inconsistent with its kubebuilder default: the
comment shows "10%" but the kubebuilder default is `"7%"`; update the comment so
the example matches the actual default (or vice versa) to avoid confusion.
Locate the EvictionHardMemory field in the Kubelet type and change the inline
comment/example text to match the +kubebuilder:default value `"7%"` (or update
the +kubebuilder:default to `"10%"` if you intend the example to be the
default), and ensure any related upstream values.yaml/CRD description that
generates this comment is kept consistent with the chosen default; also verify
EvictionSoftMemory remains `"10%"` so the pair still illustrates distinct
soft/hard defaults.
- Around line 227-228: The NodeGroup struct currently declares a non-pointer
Kubelet field (Kubelet Kubelet `json:"kubelet"`) which makes `kubelet` required
in the generated CRD/OpenAPI; fix by making the field optional or providing a
default: either change the declaration to a pointer or add `omitempty` to the
JSON tag (e.g., Kubelet *Kubelet `json:"kubelet,omitempty"`), or add a
kubebuilder default marker (`+kubebuilder:default:={}`) on the Kubelet field in
the upstream source (values.yaml) and then regenerate the types so the CRD no
longer lists `kubelet` as required; update the NodeGroup/Kubelet definition and
run the codegen/regeneration step to apply the change.
In `@packages/apps/kubernetes/README.md`:
- Line 112: The README example for nodeGroups[name].kubelet.evictionHardMemory
is inconsistent with the type definition: change the example value from "7%" to
"10%" so it matches the description in api/apps/v1alpha1/kubernetes/types.go and
does not collide with the actual default; update the corresponding comment in
the upstream values.yaml (which the README is generated from) to use "10%" as
the example and confirm the default value is still documented as "7%" in
types.go if that remains the true default, or update types.go to reflect the
actual default if it should be 10% so both the Default and Example align. Ensure
nodeGroups[name].kubelet.evictionHardMemory is consistently documented across
README, values.yaml comment, and api/apps/v1alpha1/kubernetes/types.go.
---
Outside diff comments:
In `@packages/apps/kubernetes/values.schema.json`:
- Around line 27-36: The schema change made `kubelet` mandatory by adding it to
additionalProperties.required in values.schema.json which will break existing
tenant Kubernetes resources; either remove "kubelet" from the per-node-group
required list in the source values.yaml used by cozyvalues-gen so node groups
remain optional, or add a kubebuilder default on the NodeGroup.Kubelet field
(add +kubebuilder:default:={} to NodeGroup.Kubelet in
api/apps/v1alpha1/kubernetes/types.go) and re-run cozyvalues-gen to regenerate
values.schema.json so existing configs are auto-populated. Ensure you pick one
approach (prefer fixing types.go and regenerating) and do not edit the generated
JSON directly.
In `@packages/system/kubernetes-rd/cozyrds/kubernetes.yaml`:
- Around line 11-29: The generated Kubernetes manifest’s embedded openAPISchema
and keysOrder were changed by pre-commit; run the repository generator and
commit the regenerated output: run make generate locally to update the
openAPISchema (the large Chart Values JSON object) and the keysOrder array,
verify the kubernetes.yaml changes (the Chart Values / keysOrder blocks shown in
the diff), then add and commit the regenerated file so the pre-commit hook no
longer rewrites kubernetes.yaml.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 748d7879-a0b2-469f-9e45-5489f3799426
📒 Files selected for processing (6)
api/apps/v1alpha1/kubernetes/types.gopackages/apps/kubernetes/README.mdpackages/apps/kubernetes/templates/cluster.yamlpackages/apps/kubernetes/values.schema.jsonpackages/apps/kubernetes/values.yamlpackages/system/kubernetes-rd/cozyrds/kubernetes.yaml
✅ Files skipped from review due to trivial changes (1)
- packages/apps/kubernetes/values.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/apps/kubernetes/templates/cluster.yaml
| | `nodeGroups[name].kubelet.kubeReservedMemory` | Memory reserved for kubelet and container runtime. Auto-computed from instanceType if empty. | `string` | `""` | | ||
| | `nodeGroups[name].kubelet.systemReservedCpu` | CPU reserved for host OS. | `string` | `100m` | | ||
| | `nodeGroups[name].kubelet.kubeReservedCpu` | CPU reserved for kubelet and container runtime. | `string` | `100m` | | ||
| | `nodeGroups[name].kubelet.evictionHardMemory` | Hard eviction threshold for memory (absolute like 200Mi or percentage like 7%). | `string` | `7%` | |
There was a problem hiding this comment.
Minor doc copy inconsistency for evictionHardMemory.
The description here reads "percentage like 7%", while api/apps/v1alpha1/kubernetes/types.go (line 191) documents the same field as "percentage like 10%". Using 7% in the example is also confusing because 7% is already this field's default — the example and the default become indistinguishable. Consider aligning both sources (e.g., use 10% in the example everywhere), updated at the upstream values.yaml comment since this README is auto-generated.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/apps/kubernetes/README.md` at line 112, The README example for
nodeGroups[name].kubelet.evictionHardMemory is inconsistent with the type
definition: change the example value from "7%" to "10%" so it matches the
description in api/apps/v1alpha1/kubernetes/types.go and does not collide with
the actual default; update the corresponding comment in the upstream values.yaml
(which the README is generated from) to use "10%" as the example and confirm the
default value is still documented as "7%" in types.go if that remains the true
default, or update types.go to reflect the actual default if it should be 10% so
both the Default and Example align. Ensure
nodeGroups[name].kubelet.evictionHardMemory is consistently documented across
README, values.yaml comment, and api/apps/v1alpha1/kubernetes/types.go.
There was a problem hiding this comment.
This is by design — the doc comment describes the accepted format, and 7% is a valid representative example. The default value is documented separately via the +kubebuilder:default marker.
There was a problem hiding this comment.
@Arsolitt, understood — using 7% in the description purely as a format illustration is perfectly reasonable, and with the default handled separately by the +kubebuilder:default marker, there's no actual ambiguity. Thanks for the clarification!
🧠 Learnings used
Learnt from: lexfrei
Repo: cozystack/cozystack PR: 2073
File: packages/apps/kubernetes/files/versions.yaml:1-1
Timestamp: 2026-02-19T13:25:32.183Z
Learning: In packages/apps/kubernetes/files/versions.yaml, Kubernetes patch versions are intentionally capped at Kamaji's KubeadmVersion (e.g., v1.35.0 in edge-26.2.4). The update-versions.sh script enforces this limit as a safety measure—Kamaji only supports Kubernetes versions up to the kubeadm binary version it ships with. Using newer patch releases (like v1.35.1) would exceed Kamaji's supported boundary.
Learnt from: lexfrei
Repo: cozystack/cozystack PR: 2058
File: packages/apps/harbor/README.md:3-3
Timestamp: 2026-02-16T18:38:50.741Z
Learning: In readme files within packages that are auto-generated by cozyvalues-gen (e.g., packages/apps/harbor/README.md), do not edit them manually. They are produced from upstream sources or configuration, and manual changes will be overwritten by the generator. If you need documentation changes, update the upstream source or generator config and regenerate.
| {{- end }} | ||
| {{- end }} | ||
| {{/* Compute kubelet reservations: explicit resources.memory wins over instanceType.spec.memory.guest. | ||
| Auto-computed system/kube reserved = 5% of effective memory, clamped to [64Mi, 1Gi]. */}} |
There was a problem hiding this comment.
64Mi is very, very small. A Go binary easily requires the absolute minimum of 2x the binary size and that's not even beginning to factor in the operational requirements, that's just loading the binary into memory and giving the garbage collector breathing room. I would set a minimum memory requirement of 256Mi.
There was a problem hiding this comment.
Agreed, the minimum clamp was raised to 256Mi. Current logic: min(max(5% of effective memory, 256) 1024) — so the range is [256Mi, 1Gi].
| {{- $effectiveMemory := "" }} | ||
| {{- if and $group.resources $group.resources.memory }} | ||
| {{- $effectiveMemory = $group.resources.memory | toString }} | ||
| {{- else if and $instanceType $instanceType.spec $instanceType.spec.memory $instanceType.spec.memory.guest }} | ||
| {{- $effectiveMemory = $instanceType.spec.memory.guest | toString }} | ||
| {{- end }} | ||
| {{- $autoReservedMi := 64 }} | ||
| {{- if $effectiveMemory }} | ||
| {{- $effectiveMemMi := divf (include "cozy-lib.resources.toFloat" $effectiveMemory | float64) 1048576.0 | int }} | ||
| {{- $fivePercentMi := mulf ($effectiveMemMi | float64) 0.05 | int }} | ||
| {{- if gt $fivePercentMi $autoReservedMi }}{{- $autoReservedMi = $fivePercentMi }}{{- end }} | ||
| {{- if gt $autoReservedMi 1024 }}{{- $autoReservedMi = 1024 }}{{- end }} | ||
| {{- end }} | ||
| {{- $kubeletOverride := $group.kubelet | default dict }} | ||
| {{- $systemReservedMemory := $kubeletOverride.systemReservedMemory | default (printf "%dMi" $autoReservedMi) }} | ||
| {{- $kubeReservedMemory := $kubeletOverride.kubeReservedMemory | default (printf "%dMi" $autoReservedMi) }} | ||
| {{- $systemReservedCpu := $kubeletOverride.systemReservedCpu | default "100m" }} | ||
| {{- $kubeReservedCpu := $kubeletOverride.kubeReservedCpu | default "100m" }} | ||
| {{- $evictionHardMemory := $kubeletOverride.evictionHardMemory | default "7%" }} | ||
| {{- $evictionSoftMemory := $kubeletOverride.evictionSoftMemory | default "10%" }} | ||
| {{/* Validate reservation field formats */}} | ||
| {{- if and $kubeletOverride.systemReservedMemory (not (regexMatch `^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei)?$` ($kubeletOverride.systemReservedMemory | toString))) }} | ||
| {{- fail (printf "nodeGroup %s: invalid systemReservedMemory value %q — must be a valid Kubernetes memory quantity (e.g. 128Mi, 1Gi)" $groupName ($kubeletOverride.systemReservedMemory | toString)) }} | ||
| {{- end }} | ||
| {{- if and $kubeletOverride.kubeReservedMemory (not (regexMatch `^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei)?$` ($kubeletOverride.kubeReservedMemory | toString))) }} | ||
| {{- fail (printf "nodeGroup %s: invalid kubeReservedMemory value %q — must be a valid Kubernetes memory quantity (e.g. 128Mi, 1Gi)" $groupName ($kubeletOverride.kubeReservedMemory | toString)) }} | ||
| {{- end }} | ||
| {{- if and $kubeletOverride.systemReservedCpu (not (regexMatch `^[0-9]+(\.[0-9]+)?m?$` ($kubeletOverride.systemReservedCpu | toString))) }} | ||
| {{- fail (printf "nodeGroup %s: invalid systemReservedCpu value %q — must be a valid Kubernetes CPU quantity (e.g. 100m, 0.5, 1)" $groupName ($kubeletOverride.systemReservedCpu | toString)) }} | ||
| {{- end }} | ||
| {{- if and $kubeletOverride.kubeReservedCpu (not (regexMatch `^[0-9]+(\.[0-9]+)?m?$` ($kubeletOverride.kubeReservedCpu | toString))) }} | ||
| {{- fail (printf "nodeGroup %s: invalid kubeReservedCpu value %q — must be a valid Kubernetes CPU quantity (e.g. 100m, 0.5, 1)" $groupName ($kubeletOverride.kubeReservedCpu | toString)) }} | ||
| {{- end }} | ||
| {{- if and (hasSuffix "%" $evictionHardMemory) (hasSuffix "%" $evictionSoftMemory) }} | ||
| {{- $hardPct := trimSuffix "%" $evictionHardMemory | float64 }} | ||
| {{- $softPct := trimSuffix "%" $evictionSoftMemory | float64 }} | ||
| {{- if ge $hardPct $softPct }} | ||
| {{- fail (printf "nodeGroup %s: evictionHardMemory (%s) must be strictly less than evictionSoftMemory (%s)" $groupName $evictionHardMemory $evictionSoftMemory) }} | ||
| {{- end }} | ||
| {{- else if and (not (hasSuffix "%" $evictionHardMemory)) (not (hasSuffix "%" $evictionSoftMemory)) }} | ||
| {{- $hardBytes := include "cozy-lib.resources.toFloat" $evictionHardMemory | float64 }} | ||
| {{- $softBytes := include "cozy-lib.resources.toFloat" $evictionSoftMemory | float64 }} | ||
| {{- if ge $hardBytes $softBytes }} | ||
| {{- fail (printf "nodeGroup %s: evictionHardMemory (%s) must be strictly less than evictionSoftMemory (%s)" $groupName $evictionHardMemory $evictionSoftMemory) }} | ||
| {{- end }} | ||
| {{- else }} | ||
| {{- fail (printf "nodeGroup %s: evictionHardMemory and evictionSoftMemory must use the same unit type (both percentage or both absolute) — got evictionHardMemory=%s, evictionSoftMemory=%s" $groupName $evictionHardMemory $evictionSoftMemory) }} | ||
| {{- end }} |
There was a problem hiding this comment.
This is a lot of horribly hard to read go-template programming. Consider extracting some parts of this to helper templates in cozy-lib. Non-blocking, but I've a hunch that parts of these calculations could be simplified.
There was a problem hiding this comment.
Acknowledged. The cpuToMillicores helper was extracted to cozy-lib, but the bulk of the reservation logic remains inline. A larger extraction is worth doing as a separate effort to keep this PR focused.
|
A few readability suggestions for the new templating block in 1. Collapse the four regex validation blocks (lines 215–226). They're near-duplicates across 2. Use sprig 3. Compute 4. Simplify eviction validation (lines 227–241). Three branches with duplicated error-message strings can become: classify each value once ( 5. Minor: the None of these change behavior; they're pure readability/maintainability wins. |
|
A few additional issues from a fresh pass over the branch (ones not yet raised): Blocking 1. Missing format validation for
2. README version range is stale. Non-blocking 3. No cross-field sanity check on reservation totals. A user can set 4. Reservation memory regex rejects valid Kubernetes forms. 5. CPU regex accepts |
…d add CPU annotation tests Signed-off-by: Arsolitt <[email protected]>
…on wording Signed-off-by: Arsolitt <[email protected]>
Assisted-By: Claude <[email protected]> Signed-off-by: Arsolitt <[email protected]>
Remove cgroups-per-qos, cgroup-driver, and kernel-memcg-notification from kubeletExtraArgs. These flags are deprecated or already removed from kubelet CLI, and their functionality is either the default behavior or handled via KubeletConfiguration. Signed-off-by: Arsolitt <[email protected]>
…tion error messages Signed-off-by: Arsolitt <[email protected]>
…rror message Signed-off-by: Arsolitt <[email protected]>
…ages without effective memory Signed-off-by: Arsolitt <[email protected]>
929cf6a to
8aedc35
Compare
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
LGTM overall — the reservation logic, validation, and test coverage look solid, and this fixes a real production failure mode (kubelet OOM → NotReady). A few follow-ups that would be nice to land before or shortly after merge:
Worth fixing in this PR if you have the bandwidth:
cluster.yaml:468andcluster.yaml:477— the capacity annotations use| ceil, which rounds up from computed allocatable. For the defaultu1.medium(4Gi), real allocatable is ~3297.28Mi but the annotation advertises3298Mi, so cluster-autoscaler can provision a node for a pod that kubelet will never schedule. The discrepancy is always <1Mi, butfloor(or plaininttruncation) would make the annotation a safe under-approximation instead of an overstatement. CPU path is unaffected becausecpuToMillicoresalready returns integers.
Good follow-up PRs (not blockers):
- The ~160 lines of inline reservation/validation/allocatable math in
cluster.yamlare hard to audit.cpuToMillicoresis already a helper — extracting the rest of the arithmetic (effective-capacity resolution, eviction parsing, reservation guards) intocozy-libwould make it reviewable and reusable. - All settings go through
kubeletExtraArgs(CLI flags). With the 1.36/1.37 deprecations landing upstream, migrating to aKubeletConfigurationpatch for kubeadm is the sustainable long-term path — kamaji already does this for the control-plane side.
Thanks for the thorough iteration on this one — the test suite is particularly nice.
…tion cluster-autoscaler relies on the capacity annotation to decide whether a node can fit a pending pod. ceil rounds the allocatable value up by <1Mi, which can advertise capacity that kubelet will never actually schedule. floor produces a safe under-approximation instead. Signed-off-by: Arsolitt <[email protected]>
Merge origin/main into feat/kubernetes-kubelet-reserved-resources. Conflicts resolved: - packages/apps/kubernetes/Makefile: kept .PHONY declaration from PR - packages/system/kubernetes-rd/cozyrds/kubernetes.yaml: kept both kubelet schema from PR and images property from main Signed-off-by: Arsolitt <[email protected]>
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
LGTM — picking up where my dismissed LGTM left off, the substantive concern about the memory capacity annotation rounding up has been addressed: cluster.yaml:499 now uses divf $allocatableBytes 1048576.0 | floor, so the annotation is a safe under-approximation rather than an overstatement that could fool cluster-autoscaler into provisioning a node for a pod kubelet won't schedule. CPU capacity (cluster.yaml:508) uses | ceil | int but since cpuToMillicores already returns integers the rounding is a no-op — safe.
Other improvements since the previous round are also clean:
- 70ac79d subtracts CPU reservations from the capacity annotation, matching kubelet's allocatable view.
- Reservation-validation regex now accepts all valid K8s quantity forms (6639e5c) and includes eviction threshold in allocatable memory (937a27b).
- 1702c14 auto-computes CPU reservations from node capacity, mirroring the memory path's clamp pattern.
The earlier good-follow-up suggestions (extracting reservation/validation/allocatable math to cozy-lib, migrating from kubeletExtraArgs to KubeletConfiguration) are still worth doing, but remain follow-ups rather than blockers.
Merge origin/main into feat/kubernetes-kubelet-reserved-resources. PR #2454 (persistent storage for worker nodes) landed in main and touched the same nodeGroups schema. Conflicts resolved by keeping both feature sets (kubelet reservations + persistent storage): - api/apps/v1alpha1/kubernetes/types.go: merged kubebuilder default marker with all keys from both features - packages/apps/kubernetes/README.md: merged Parameters table with diskSize/storageClass + kubelet* rows - packages/system/kubernetes-rd/cozyrds/kubernetes.yaml: merged openAPISchema and keysOrder, dropped stale ephemeralStorage path Signed-off-by: Arsolitt <[email protected]>
PR #2454 renamed the nodeGroups field ephemeralStorage to diskSize and added a migration guard that fails template rendering when the old name is used. Update kubelet reservation tests to use the new field name. Signed-off-by: Arsolitt <[email protected]>
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
LGTM — verified locally on 22d2a22: helm unittest . in packages/apps/kubernetes passes 98/98 across all 5 test suites (admin-kubeconfig waits, cluster.yaml template, GPU Operator/HAMi integration, HAMi HelmRelease, kubelet-reservation). The substantive PR-specific code (kubelet reservation logic in cluster.yaml, the _resources.tpl helper, floor for memory and ceil | int for CPU on the cluster-autoscaler capacity annotations) is unchanged from my earlier LGTM.
Changes since the previous round:
- d4808b9 merges main and pulls in already-merged work (#2454 persistent storage, #2466 postgres params, #2541 harbor user-secret, #2542 TenantSecret labelSelector, #2414 seaweedfs PUT limit, GPU dashboards). No PR-2420 logic touched.
- 22d2a22 renames the test fixtures from
ephemeralStoragetodiskSizeto track the field rename merged in #2454. Without this, the test suite would fail Helm schema validation; with it, all assertions pass.
What this PR does
Adds configurable kubelet resource reservations and eviction thresholds for tenant cluster worker nodes. Without explicit reservations and cgroup enforcement, the scheduler treats nearly all node memory and CPU as allocatable. Under pressure, the OOM killer can target kubelet itself because no cgroup boundaries protect it, leading to "Kubelet stopped posting node status" failures and unrecoverable NotReady nodes.
This PR auto-computes per-node-group kubelet reservations from the effective node capacity (instanceType or explicit
resources):systemReservedMemoryandkubeReservedMemoryeach default to 5% of effective memory, clamped 256Mi-1GisystemReservedCpuandkubeReservedCpueach default to 5% of effective CPU, clamped 50m-500mevictionHardMemory(default 7%) andevictionSoftMemory(default 10%) with validation that hard < softnodefs.available,imagefs.available, andnodefs.inodesFreethresholds are preserved alongside memory signalsenforce-node-allocatable=podsconfigures advisory-only reservations — the scheduler accounts for reserved resources when computing allocatable capacity, but no cgroup boundaries are enforced for system or kube reserved resourcesAll reservation fields are optional and overridable per node group via
.nodeGroups[].kubelet.Default rendered kubeletExtraArgs:
New values schema:
Release note: