Skip to content

feat(kubernetes): add kubelet resource reservations for worker nodes - #2420

Merged
Arsolitt (Arsolitt) merged 49 commits into
mainfrom
feat/kubernetes-kubelet-reserved-resources
May 1, 2026
Merged

Arsolitt (Arsolitt) merged 49 commits into
mainfrom
feat/kubernetes-kubelet-reserved-resources

Conversation

@IvanHunters

@IvanHunters IvanHunters commented Apr 18, 2026 •

Copy link
Copy Markdown
Collaborator

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):

  • Memory: systemReservedMemory and kubeReservedMemory each default to 5% of effective memory, clamped 256Mi-1Gi
  • CPU: systemReservedCpu and kubeReservedCpu each default to 5% of effective CPU, clamped 50m-500m
  • Eviction: configurable evictionHardMemory (default 7%) and evictionSoftMemory (default 10%) with validation that hard < soft
  • Disk eviction: nodefs.available, imagefs.available, and nodefs.inodesFree thresholds are preserved alongside memory signals
  • Capacity annotations: cluster-autoscaler annotations now report allocatable memory and CPU (total minus all reservations and eviction threshold) instead of raw totals
  • Cgroup enforcement: enforce-node-allocatable=pods configures advisory-only reservations — the scheduler accounts for reserved resources when computing allocatable capacity, but no cgroup boundaries are enforced for system or kube reserved resources
  • Validation: format checks, zero-value rejection, eviction ordering (hard < soft), same-unit-type requirement for eviction thresholds, and total reservation sanity check against effective node capacity

All reservation fields are optional and overridable per node group via .nodeGroups[].kubelet.

Default rendered kubeletExtraArgs:

kubeletExtraArgs:
  system-reserved: "memory=256Mi,cpu=50m"
  kube-reserved: "memory=256Mi,cpu=50m"
  eviction-hard: "memory.available<7%,nodefs.available<10%,imagefs.available<15%,nodefs.inodesFree<5%"
  eviction-soft: "memory.available<10%,nodefs.available<15%,imagefs.available<20%"
  eviction-soft-grace-period: "memory.available=1m30s,nodefs.available=1m30s,imagefs.available=1m30s"
  eviction-minimum-reclaim: "memory.available=256Mi"
  enforce-node-allocatable: "pods"

New values schema:

nodeGroups:
  md0:
    kubelet:
      systemReservedMemory: ""   # auto-computed (5% of effective memory, 256Mi-1Gi)
      kubeReservedMemory: ""     # auto-computed (5% of effective memory, 256Mi-1Gi)
      systemReservedCpu: ""      # auto-computed (5% of effective CPU, 50m-500m)
      kubeReservedCpu: ""        # auto-computed (5% of effective CPU, 50m-500m)
      evictionHardMemory: "7%"   # absolute (e.g. 200Mi) or percentage
      evictionSoftMemory: "10%"  # must be > evictionHardMemory, same unit type

Release note:

feat(kubernetes): add per-node-group kubelet resource reservations with auto-computed system/kube reserved memory (5% of effective memory, clamped 256Mi-1Gi) and CPU (5% of effective CPU, clamped 50m-500m), configurable memory eviction thresholds, preserved disk eviction signals (nodefs, imagefs, inodesFree), pods-only cgroup enforcement, and reservation-aware allocatable capacity annotations for cluster-autoscaler.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization's overage spend limit has been reached.

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.

@dosubot dosubot Bot added the size/XS This PR changes 0-9 lines, ignoring generated files label Apr 18, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • Resource Reservations: Implemented explicit system and kube-reserved resource allocations to protect critical node processes.
  • Hard Cgroup Enforcement: Enabled enforce-node-allocatable to prevent system processes from exceeding reserved limits, effectively stopping OOM-related kubelet crashes.
  • Eviction Policies: Configured tiered memory eviction thresholds (soft and hard) with minimum reclaim guarantees to improve node stability under pressure.

🧠 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 Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@coderabbitai

coderabbitai Bot commented Apr 18, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Per-node-group Helm templates now compute effective CPU/memory (resolving instanceType early), auto-derive kubelet reservations and eviction thresholds, and render explicit joinConfiguration.nodeRegistration.kubeletExtraArgs; CRD/types, schema, docs, tests, and deepcopy helpers were added/updated to expose and validate kubelet settings.

Changes

Cohort / File(s) Summary
Helm template (kubelet flags & instanceType resolution)
packages/apps/kubernetes/templates/cluster.yaml
Resolve instanceType earlier per nodeGroup, compute effective memory/CPU, auto-calc/clamp kubelet reservations and eviction values, validate inputs at render-time, and replace empty kubeletExtraArgs with explicit kubelet flags (reservations, eviction settings, cgroup/allocatable args). Removed duplicate instanceType lookup and adjusted cluster-autoscaler memory annotation computation.
Helm unit tests
packages/apps/kubernetes/tests/kubelet-reservation_test.yaml
Added extensive tests covering defaults, overrides, absolute/percentage eviction handling and validation, per-nodeGroup isolation, capacity annotation behavior, CPU clamp/validation, and negative cases (e.g., missing instanceType).
API types & deepcopy
api/apps/v1alpha1/kubernetes/types.go, api/apps/v1alpha1/kubernetes/zz_generated.deepcopy.go
Added new Kubelet type and NodeGroup.Kubelet field; updated kubebuilder defaults. Generated DeepCopy methods for Kubelet and adjusted NodeGroup deepcopy to preserve the field.
Values & schema
packages/apps/kubernetes/values.yaml, packages/apps/kubernetes/values.schema.json
Introduced kubelet object in nodeGroup typedef and defaults (kubelet: {}); added fields for systemReservedMemory, kubeReservedMemory, systemReservedCpu, kubeReservedCpu, evictionHardMemory (default 7%), evictionSoftMemory (default 10%) and corresponding schema entries/defaults.
Documentation
packages/apps/kubernetes/README.md
Documented new nodeGroups[name].kubelet parameters, auto-computation/clamping behavior, capacity annotation semantics, and updated supported Kubernetes patch range.
Chart OpenAPI / packaging
packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
Updated embedded chart OpenAPI schema and defaults to include kubelet: {} for nodeGroups and added kubelet properties to schema and keys ordering.
Build/test helper
packages/apps/kubernetes/Makefile
Added test target to run helm unittest ..

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through templates, nibbled at bytes,
I balanced small reserves to keep sleepy nights,
Evictions polite, cgroups all aligned,
Each node purrs softly, tidy and kind.
Hooray — the cluster naps, all cozy and bright!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(kubernetes): add kubelet resource reservations for worker nodes' accurately and concisely summarizes the main change in the changeset—introducing configurable kubelet resource reservations and eviction thresholds for worker nodes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/kubernetes-kubelet-reserved-resources

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 and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +260 to +263
enforce-node-allocatable: "pods,system-reserved,kube-reserved"
cgroups-per-qos: "true"
cgroup-driver: "systemd"
kernel-memcg-notification: "true"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

There are several issues in this block:

  1. Missing Cgroup Paths: When enforce-node-allocatable includes system-reserved and kube-reserved, the kubelet requires the --system-reserved-cgroup and --kube-reserved-cgroup flags to be specified. Without them, the kubelet will fail to start.
  2. Removed Flags: The flags cgroups-per-qos, cgroup-driver, and kernel-memcg-notification were 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 via KubeletConfiguration (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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +254 to +255
system-reserved: "cpu=100m,memory=512Mi"
kube-reserved: "cpu=100m,memory=512Mi"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

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
  1. PR body must contain a release-note block with type(scope): human-readable changelog entry. (link)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The release-note block is present in the PR description.

@IvanHunters
IvanHunters force-pushed the feat/kubernetes-kubelet-reserved-resources branch from 85ccadb to 3d8147d Compare April 18, 2026 12:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between e26894e and 85ccadb.

📒 Files selected for processing (1)
  • packages/apps/kubernetes/templates/cluster.yaml

Comment thread packages/apps/kubernetes/templates/cluster.yaml Outdated
Comment thread packages/apps/kubernetes/templates/cluster.yaml Outdated
@Arsolitt
Arsolitt (Arsolitt) force-pushed the feat/kubernetes-kubelet-reserved-resources branch from 3d8147d to 51ae51f Compare April 21, 2026 13:40
@dosubot dosubot Bot added size/M This PR changes 30-99 lines, ignoring generated files and removed size/XS This PR changes 0-9 lines, ignoring generated files labels Apr 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 | 🟠 Major

Breaking change: kubelet is now required on every node group.

Adding kubelet to additionalProperties.required means any existing tenant Kubernetes resource with user-defined node groups beyond md0 (which only gets kubelet: {} via the defaults for that specific key) will fail schema validation unless each node group explicitly sets a kubelet field. Since Kubelet in api/apps/v1alpha1/kubernetes/types.go has no +kubebuilder:default:={}, the API server will not auto-populate it either.

Consider either (a) dropping kubelet from the per-node-group required list and treating it as optional (the Helm template can fall back to defaults when absent), or (b) adding a +kubebuilder:default:={} marker on NodeGroup.Kubelet in types.go and regenerating, so existing configs continue to apply.

Note: this file is regenerated by cozyvalues-gen; fix the upstream values.yaml / types.go rather 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 | 🟠 Major

Pre-commit make generate modified this file — regenerate and commit.

The pre-commit hook reports this file was rewritten by make generate. Since the embedded openAPISchema and keysOrder are derived from the chart's values.yaml / values.schema.json, please rerun make generate locally 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3d8147d and 51ae51f.

📒 Files selected for processing (6)
  • api/apps/v1alpha1/kubernetes/types.go
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/values.schema.json
  • packages/apps/kubernetes/values.yaml
  • packages/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

Comment thread api/apps/v1alpha1/kubernetes/types.go
Comment thread api/apps/v1alpha1/kubernetes/types.go Outdated
Comment thread packages/apps/kubernetes/README.md Outdated
| `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%` |

@coderabbitai coderabbitai Bot Apr 21, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@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.

@Arsolitt
Arsolitt (Arsolitt) marked this pull request as draft April 21, 2026 13:56
@Arsolitt Arsolitt (Arsolitt) changed the title fix(kubernetes): add kubelet reserved resources and cgroup enforcement feat(kubernetes): add kubelet resource reservations for worker nodes Apr 21, 2026
{{- 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]. */}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agreed, the minimum clamp was raised to 256Mi. Current logic: min(max(5% of effective memory, 256) 1024) — so the range is [256Mi, 1Gi].

Comment on lines +194 to +241
{{- $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 }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@lllamnyp

Copy link
Copy Markdown
Member

A few readability suggestions for the new templating block in packages/apps/kubernetes/templates/cluster.yaml (most impactful first):

1. Collapse the four regex validation blocks (lines 215–226). They're near-duplicates across system/kube × memory/cpu. A single range over a list of (fieldName, value, regex) tuples halves the line count and keeps the error-message format in one place.

2. Use sprig min/max for the $autoReservedMi clamp (lines 200–206) instead of two if gt branches:

{{- $autoReservedMi = min 1024 (max 64 (mulf ($memMi | float64) 0.05 | int)) }}

3. Compute $effectiveCpu alongside $effectiveMemory and reuse both in the capacity annotations. The and $instanceType $instanceType.spec $instanceType.spec.memory $instanceType.spec.memory.guest chain is currently repeated in the annotation block (lines 356–365); sprig's dig makes the initial resolution a one-liner, and the annotation block collapses to two plain if $effectiveX branches.

4. Simplify eviction validation (lines 227–241). Three branches with duplicated error-message strings can become: classify each value once ($hardIsPct := hasSuffix \"%\" ...), fail on type mismatch, then run one numeric comparison. Cuts the block roughly in half and DRYs the failure strings.

5. Minor: the $kubeletOverride.systemReservedMemory-style guards are intentional (you want to validate the user-supplied value, not the resolved default). Worth a one-line comment saying so — it's easy to misread as a bug and "fix" it into validating $systemReservedMemory.

None of these change behavior; they're pure readability/maintainability wins.

@lllamnyp

Copy link
Copy Markdown
Member

A few additional issues from a fresh pass over the branch (ones not yet raised):

Blocking

1. Missing format validation for evictionHardMemory / evictionSoftMemory. Commit 90656aa added regex validation for the four reservation fields but not the two eviction fields, so malformed values are silently accepted. Two failure modes:

  • evictionHardMemory: "banana", evictionSoftMemory: "garbage" — both miss the % suffix so we fall into the absolute-units branch. cozy-lib.resources.toFloat returns 0 for unparseable strings (via sprig float64 → cast.ToFloat64), so both compute to 0, ge 0 0 is true, and the user sees evictionHardMemory (banana) must be strictly less than evictionSoftMemory (garbage) — the wrong error.

  • evictionHardMemory: "banana", evictionSoftMemory: "1Gi" — 0 < 2^30, validation passes entirely, and the rendered KubeadmConfigTemplate ships memory.available<banana to kubelet. helm template succeeds; the failure surfaces only at node boot as a silent provisioning failure.

    Fix is symmetric to the reservation validation: accept ^[0-9]+(\.[0-9]+)?(%|Ki|Mi|Gi|Ti|Pi|Ei)?$ (or similar) and emit the same error shape. Tests should mirror the existing rejects invalid memory format for systemReservedMemory cases.

2. README version range is stale. packages/apps/kubernetes/README.md:15 says "Users can select the latest patch versions from 1.28 to 1.33." The Version enum in values.yaml (and the schema/types) declares v1.30–v1.35. Pre-existing drift, but the README is being actively edited in this PR — worth fixing the sentence or dropping the concrete numbers and pointing at the version field.

Non-blocking

3. No cross-field sanity check on reservation totals. A user can set systemReservedMemory: 4Gi + kubeReservedMemory: 4Gi on a 4Gi node and get zero allocatable memory. kubelet will eventually surface this, but a template-side check like "system + kube + evictionHard ≤ effectiveMemory × 0.9" would catch the footgun at helm template time. If not adding the check, a docs note putting the burden on the operator would help.

4. Reservation memory regex rejects valid Kubernetes forms. ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei)?$ rejects base-10 (1G, 500M) and exponent (1e9) quantities that resource.ParseQuantity accepts. Not common, but the error message locks users into Mi/Gi. Either broaden the regex or leave the message as-is (it's accurate to what you accept).

5. CPU regex accepts 0m and 0. Cosmetic. Could be ^([1-9][0-9]*|[0-9]+\.[0-9]+)m?$ if you want to reject zero.

@IvanHunters
IvanHunters marked this pull request as ready for review April 22, 2026 13:35
@dosubot dosubot Bot added size/L This PR changes 100-499 lines, ignoring generated files kind/feature Categorizes issue or PR as related to a new feature and removed size/M This PR changes 30-99 lines, ignoring generated files labels Apr 22, 2026
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]>
…ages without effective memory

Signed-off-by: Arsolitt <[email protected]>
@Arsolitt
Arsolitt (Arsolitt) force-pushed the feat/kubernetes-kubelet-reserved-resources branch from 929cf6a to 8aedc35 Compare April 23, 2026 15:07

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:468 and cluster.yaml:477 — the capacity annotations use | ceil, which rounds up from computed allocatable. For the default u1.medium (4Gi), real allocatable is ~3297.28Mi but the annotation advertises 3298Mi, so cluster-autoscaler can provision a node for a pod that kubelet will never schedule. The discrepancy is always <1Mi, but floor (or plain int truncation) would make the annotation a safe under-approximation instead of an overstatement. CPU path is unaffected because cpuToMillicores already returns integers.

Good follow-up PRs (not blockers):

  • The ~160 lines of inline reservation/validation/allocatable math in cluster.yaml are hard to audit. cpuToMillicores is already a helper — extracting the rest of the arithmetic (effective-capacity resolution, eviction parsing, reservation guards) into cozy-lib would make it reviewable and reusable.
  • All settings go through kubeletExtraArgs (CLI flags). With the 1.36/1.37 deprecations landing upstream, migrating to a KubeletConfiguration patch 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.

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Apr 24, 2026
…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]>
@github-actions github-actions Bot added the area/kubernetes Issues or PRs related to the tenant Kubernetes app label Apr 27, 2026
@Arsolitt Arsolitt (Arsolitt) added the do-not-merge/hold Indicates that a PR should not merge because someone has issued /hold label Apr 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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]>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 ephemeralStorage to diskSize to track the field rename merged in #2454. Without this, the test suite would fail Helm schema validation; with it, all assertions pass.

@Arsolitt Arsolitt (Arsolitt) removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued /hold label Apr 30, 2026
@Arsolitt
Arsolitt (Arsolitt) merged commit 5786afe into main May 1, 2026
12 of 13 checks passed
@Arsolitt
Arsolitt (Arsolitt) deleted the feat/kubernetes-kubelet-reserved-resources branch May 1, 2026 11:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/kubernetes Issues or PRs related to the tenant Kubernetes app kind/feature Categorizes issue or PR as related to a new feature lgtm This PR has been approved by a maintainer size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants