Skip to content

feat(kubernetes): per-node-group nodeHealthCheck overrides - #3053

Merged
myasnikovdaniil merged 1 commit into
mainfrom
feat/kubernetes-per-nodegroup-nodehealthcheck
Jul 13, 2026
Merged

feat(kubernetes): per-node-group nodeHealthCheck overrides#3053
myasnikovdaniil merged 1 commit into
mainfrom
feat/kubernetes-per-nodegroup-nodehealthcheck

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Adds per-node-group overrides for the worker MachineHealthCheck tuning introduced as a cluster-wide nodeHealthCheck by #2931.

What: optional nodeGroups[name].maxUnhealthy and nodeGroups[name].nodeStartupTimeout; when unset a group inherits the cluster-wide nodeHealthCheck.* (no behaviour change). The per-group value reuses the existing int-or-string validation; an invalid override fails the render naming the offending group.

Why: the MachineHealthCheck is rendered per node group, so groups can warrant different remediation tolerances (e.g. a stateful group at 0%, stateless at 50%). This is the per-group granularity of the standalone proposal (#2752 / #2935), rebuilt on top of #2931's nodeHealthCheck.

Notes: stacked on phase1-talos-migration (#2931) — retarget to main once it merges. Supersedes #2935 (original author credited as co-author). helm-unittest covers per-group override (percent + bare int), nodeStartupTimeout override, fallback-when-unset, and invalid-override rejection.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added per-node-group overrides for health-check settings: maxUnhealthy and nodeStartupTimeout, with automatic fallback to cluster-wide nodeHealthCheck when unset.
  • Bug Fixes
    • Improved maxUnhealthy parsing/rendering to support both bare integers and percentage strings, including more precise validation errors for the exact override path.
  • Documentation
    • Updated kubernetes app docs and values schemas (including CRD chart-values schema) to document the new override fields.
  • Tests
    • Added MachineHealthCheck rendering tests for override, fallback, and invalid maxUnhealthy (e.g., "abc") cases.

@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files area/kubernetes Issues or PRs related to the tenant Kubernetes app kind/feature Categorizes issue or PR as related to a new feature labels Jun 25, 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 enables per-node-group overrides for MachineHealthCheck settings, specifically maxUnhealthy and nodeStartupTimeout. By allowing these values to be defined at the node group level, users can now tailor remediation behavior to the specific needs of different workloads (e.g., stateful vs. stateless groups). The changes include schema updates, template logic for inheritance, and robust validation to ensure configuration integrity.

Highlights

  • Per-node-group Health Check Overrides: Introduced optional maxUnhealthy and nodeStartupTimeout overrides at the node group level, allowing for granular remediation tolerances per group.
  • Template Logic and Validation: Updated the cluster template to support per-group inheritance, falling back to cluster-wide defaults if overrides are unset, and added validation to ensure override values are correctly formatted.
  • Documentation and Testing: Updated documentation and added comprehensive helm-unittest cases to verify override behavior, fallback logic, and error handling for invalid inputs.
New Features

🧠 You can now enable Memory (public preview) 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 the 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 counterproductive. 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 Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 86cff6a5-4f8b-47cb-98bd-3d9474ffdb66

📥 Commits

Reviewing files that changed from the base of the PR and between 53192e4 and db4db4a.

📒 Files selected for processing (7)
  • api/apps/v1alpha1/kubernetes/types.go
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/tests/mhc_rendering_test.yaml
  • packages/apps/kubernetes/values.schema.json
  • packages/apps/kubernetes/values.yaml
  • packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
  • api/apps/v1alpha1/kubernetes/types.go
  • packages/apps/kubernetes/values.yaml
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/values.schema.json
  • packages/apps/kubernetes/tests/mhc_rendering_test.yaml

📝 Walkthrough

Walkthrough

Adds per-node-group overrides for nodeHealthCheck.maxUnhealthy and nodeHealthCheck.nodeStartupTimeout across the Kubernetes app’s API type, Helm values/schema, chart template, CRD schema payload, tests, and README.

Changes

Per-node-group nodeHealthCheck overrides

Layer / File(s) Summary
Schema and type contracts
api/apps/v1alpha1/kubernetes/types.go, packages/apps/kubernetes/values.yaml, packages/apps/kubernetes/values.schema.json, packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
NodeGroup gains optional MaxUnhealthy and NodeStartupTimeout fields; Helm values, JSON schema, and the CRD openAPISchema add matching properties.
MachineHealthCheck template override logic
packages/apps/kubernetes/templates/cluster.yaml
The template selects per-group overrides or cluster-wide values, validates maxUnhealthy formats, and includes the selected source key in errors.
Rendering validation and documentation
packages/apps/kubernetes/tests/mhc_rendering_test.yaml, packages/apps/kubernetes/README.md
Tests cover percent and integer overrides, timeout overrides, fallback behavior, and invalid values; the README documents both fields.

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

Suggested labels: kind/api-change

Suggested reviewers: androndo, kvaps, lllamnyp, sircthulhu, ivanhunters

Sequence Diagram(s)

sequenceDiagram
  participant NodeGroupValues
  participant ClusterTemplate
  participant MachineHealthCheck
  NodeGroupValues->>ClusterTemplate: provide per-group overrides or empty values
  ClusterTemplate->>ClusterTemplate: select override or cluster-wide fallback
  ClusterTemplate->>MachineHealthCheck: render maxUnhealthy and nodeStartupTimeout
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: per-node-group overrides for Kubernetes nodeHealthCheck settings.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/kubernetes-per-nodegroup-nodehealthcheck

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Trivy (0.69.3)

Trivy execution failed: 2026-07-13T07:27:47Z FATAL Fatal error run error: fs scan error: scan error: scan failed: failed analysis: post analysis error: post analysis error: kubernetes scan error: fs filter error: fs filter error: walk error range error: stat .golangci.yml: no such file or directory: range error: stat .golangci.yml: no such file or directory


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.

@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 per-group overrides for maxUnhealthy and nodeStartupTimeout in the Kubernetes application package, allowing individual node groups to override cluster-wide health check settings. The changes span API types, Helm templates, schemas, documentation, and tests. The review feedback identifies potential template rendering failures in cluster.yaml when overrides are explicitly set to empty strings or nil, and suggests more robust checks. Additionally, the reviewer noted that the PR description needs to include a release-note block to comply with the repository guidelines.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +617 to +620
{{- if hasKey $group "maxUnhealthy" }}
{{- $maxUnhealthy = toString $group.maxUnhealthy }}
{{- $muSource = printf "nodeGroups.%s.maxUnhealthy" $groupName }}
{{- end }}

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

Using hasKey alone to check for maxUnhealthy will evaluate to true even if the value is explicitly set to an empty string ("") or nil (e.g., when a user wants to unset an override and inherit the cluster-wide default). Since an empty string does not match the regex patterns, this will trigger a template rendering failure (fail).\n\nWe can make this more robust by checking that the key is present and its string representation is not empty.

  {{- if and (hasKey $group "maxUnhealthy") (ne (toString $group.maxUnhealthy) "") }}
  {{-   $maxUnhealthy = toString $group.maxUnhealthy }}
  {{-   $muSource = printf "nodeGroups.%s.maxUnhealthy" $groupName }}
  {{- end }}
References
  1. Ensure Helm template correctness and avoid logic errors or rendering failures when handling optional/nullable values. (link)

{{- end }}
nodeStartupTimeout: {{ $.Values.nodeHealthCheck.nodeStartupTimeout }}
{{- $nodeStartupTimeout := $.Values.nodeHealthCheck.nodeStartupTimeout }}
{{- if hasKey $group "nodeStartupTimeout" }}{{ $nodeStartupTimeout = $group.nodeStartupTimeout }}{{ end }}

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

Similarly, checking hasKey for nodeStartupTimeout without verifying if it is empty or nil can lead to rendering an empty value or failing validation if the user explicitly passes an empty string or nil to fall back to the cluster-wide default.\n\nWe should ensure it only overrides when the value is non-empty.

  {{- if and (hasKey $group "nodeStartupTimeout") (ne (toString $group.nodeStartupTimeout) "") }}{{ $nodeStartupTimeout = $group.nodeStartupTimeout }}{{ end }}
References
  1. Ensure Helm template correctness and avoid logic errors or rendering failures when handling optional/nullable values. (link)

Comment thread packages/apps/kubernetes/README.md Outdated
Comment on lines +130 to +131
| `nodeGroups[name].maxUnhealthy` | Per-group override for `nodeHealthCheck.maxUnhealthy`. When unset, the cluster-wide `nodeHealthCheck.maxUnhealthy` applies. Accepts a bare integer ("0", "1", ...) or an integer percentage ("0%", "50%"). | `string` | `""` |
| `nodeGroups[name].nodeStartupTimeout` | Per-group override for `nodeHealthCheck.nodeStartupTimeout`. When unset, the cluster-wide `nodeHealthCheck.nodeStartupTimeout` applies. | `string` | `""` |

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

According to the repository guidelines, the PR description must contain a release note block in the following format:\n\ntext\nrelease-note\ntype(scope): human-readable changelog entry\n\n\n\nPlease add this block to the PR description before merging.

References
  1. PR body must contain a release-note block in the specified format. (link)

Base automatically changed from phase1-talos-migration to main June 26, 2026 15:06

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 — the per-node-group nodeHealthCheck override flow is correct and well-tested.

Verified:

  • The MachineHealthCheck renders inside range $groupName, $group := .Values.nodeGroups, and its selector targets cluster.x-k8s.io/deployment-name: <release>-<groupName>, so each override applies only to its own group with no cross-group leakage.
  • When a group omits the field, hasKey falls back cleanly to the cluster-wide nodeHealthCheck.* — no nil-deref, no behaviour change for existing clusters.
  • maxUnhealthy reuses the existing IntOrString handling: percentages quoted, bare integers unquoted, invalid input fails the render with a message naming the offending group.
  • Schema is fully in sync across types.go, values.yaml, values.schema.json, README, and the embedded openAPISchema; the codegen-drift check is green.
  • helm-unittest covers percent override, bare-int override, nodeStartupTimeout override, fallback-when-unset, and invalid-input rejection.

One non-blocking robustness point before merge: hasKey alone treats an explicitly-empty value as "set", so a group with maxUnhealthy: "" (or null) would hard-fail the render and nodeStartupTimeout: "" would render a null timeout. The sibling optional-string field storageClass already guards this with {{- with .group.storageClass }} (and values.yaml ships storageClass: ""), so the empty value is a shape the chart elsewhere expects. Suggest matching that precedent — and (hasKey $group "maxUnhealthy") (ne (toString $group.maxUnhealthy) "") (same for nodeStartupTimeout) — plus a small maxUnhealthy: "" → falls back test.

The failing E2E run is unrelated — it dies on LINSTOR controller connectivity during management-cluster bootstrap, before any tenant MachineHealthCheck is rendered; a rerun should clear it.

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/apps/kubernetes/templates/cluster.yaml`:
- Around line 680-698: Empty node-group overrides are being treated as real
values, which breaks fallback to nodeHealthCheck defaults. Update the cluster
template logic around maxUnhealthy and nodeStartupTimeout so the per-group value
is used only when it is non-empty, not just when hasKey is true; keep the
existing validation/rendering paths in the same template block and preserve
fallback to $.Values.nodeHealthCheck.* for empty strings. Add a regression test
covering nodeGroups.<name>.maxUnhealthy and nodeGroups.<name>.nodeStartupTimeout
set to empty strings to confirm they inherit the global defaults.
🪄 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: 65a9550a-591c-4b51-9837-bb46167ee5aa

📥 Commits

Reviewing files that changed from the base of the PR and between 4404c5c and eda99e7.

📒 Files selected for processing (7)
  • api/apps/v1alpha1/kubernetes/types.go
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/tests/mhc_rendering_test.yaml
  • packages/apps/kubernetes/values.schema.json
  • packages/apps/kubernetes/values.yaml
  • packages/system/kubernetes-rd/cozyrds/kubernetes.yaml

Comment on lines 680 to +698
{{- $maxUnhealthy := toString $.Values.nodeHealthCheck.maxUnhealthy }}
{{- $muSource := "nodeHealthCheck.maxUnhealthy" }}
{{- if hasKey $group "maxUnhealthy" }}
{{- $maxUnhealthy = toString $group.maxUnhealthy }}
{{- $muSource = printf "nodeGroups.%s.maxUnhealthy" $groupName }}
{{- end }}
{{- if hasSuffix "%" $maxUnhealthy }}
{{- if not (regexMatch "^[0-9]+%$" $maxUnhealthy) }}
{{- fail (printf "nodeHealthCheck.maxUnhealthy must be a bare integer (e.g. 0, 1) or an integer percentage (e.g. 50%%), got %q" $maxUnhealthy) }}
{{- fail (printf "%s must be a bare integer (e.g. 0, 1) or an integer percentage (e.g. 50%%), got %q" $muSource $maxUnhealthy) }}
{{- end }}
maxUnhealthy: {{ $maxUnhealthy | quote }}
{{- else if regexMatch "^[0-9]+$" $maxUnhealthy }}
maxUnhealthy: {{ $maxUnhealthy | int }}
{{- else }}
{{- fail (printf "nodeHealthCheck.maxUnhealthy must be a bare integer (e.g. 0, 1) or an integer percentage (e.g. 50%%), got %q" $maxUnhealthy) }}
{{- fail (printf "%s must be a bare integer (e.g. 0, 1) or an integer percentage (e.g. 50%%), got %q" $muSource $maxUnhealthy) }}
{{- end }}
nodeStartupTimeout: {{ $.Values.nodeHealthCheck.nodeStartupTimeout }}
{{- $nodeStartupTimeout := $.Values.nodeHealthCheck.nodeStartupTimeout }}
{{- if hasKey $group "nodeStartupTimeout" }}{{ $nodeStartupTimeout = $group.nodeStartupTimeout }}{{ end }}
nodeStartupTimeout: {{ $nodeStartupTimeout }}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Ignore empty-string node-group overrides when applying fallback.

Line 682 and Line 697 switch to the per-group value on key presence alone. That means nodeGroups.<name>.maxUnhealthy: "" now fails validation and nodeGroups.<name>.nodeStartupTimeout: "" renders an empty field instead of inheriting nodeHealthCheck.*, even though the new override fields are documented as optional/empty by default. Only promote the node-group value when it is non-empty, and add a regression test for the explicit-empty case.

Suggested fix
   {{- $maxUnhealthy := toString $.Values.nodeHealthCheck.maxUnhealthy }}
   {{- $muSource := "nodeHealthCheck.maxUnhealthy" }}
   {{- if hasKey $group "maxUnhealthy" }}
-  {{-   $maxUnhealthy = toString $group.maxUnhealthy }}
-  {{-   $muSource = printf "nodeGroups.%s.maxUnhealthy" $groupName }}
+  {{-   $groupMaxUnhealthy := toString $group.maxUnhealthy }}
+  {{-   if ne $groupMaxUnhealthy "" }}
+  {{-     $maxUnhealthy = $groupMaxUnhealthy }}
+  {{-     $muSource = printf "nodeGroups.%s.maxUnhealthy" $groupName }}
+  {{-   end }}
   {{- end }}
   ...
   {{- $nodeStartupTimeout := $.Values.nodeHealthCheck.nodeStartupTimeout }}
-  {{- if hasKey $group "nodeStartupTimeout" }}{{ $nodeStartupTimeout = $group.nodeStartupTimeout }}{{ end }}
+  {{- if hasKey $group "nodeStartupTimeout" }}
+  {{-   $groupNodeStartupTimeout := toString $group.nodeStartupTimeout }}
+  {{-   if ne $groupNodeStartupTimeout "" }}
+  {{-     $nodeStartupTimeout = $groupNodeStartupTimeout }}
+  {{-   end }}
+  {{- end }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{{- $maxUnhealthy := toString $.Values.nodeHealthCheck.maxUnhealthy }}
{{- $muSource := "nodeHealthCheck.maxUnhealthy" }}
{{- if hasKey $group "maxUnhealthy" }}
{{- $maxUnhealthy = toString $group.maxUnhealthy }}
{{- $muSource = printf "nodeGroups.%s.maxUnhealthy" $groupName }}
{{- end }}
{{- if hasSuffix "%" $maxUnhealthy }}
{{- if not (regexMatch "^[0-9]+%$" $maxUnhealthy) }}
{{- fail (printf "nodeHealthCheck.maxUnhealthy must be a bare integer (e.g. 0, 1) or an integer percentage (e.g. 50%%), got %q" $maxUnhealthy) }}
{{- fail (printf "%s must be a bare integer (e.g. 0, 1) or an integer percentage (e.g. 50%%), got %q" $muSource $maxUnhealthy) }}
{{- end }}
maxUnhealthy: {{ $maxUnhealthy | quote }}
{{- else if regexMatch "^[0-9]+$" $maxUnhealthy }}
maxUnhealthy: {{ $maxUnhealthy | int }}
{{- else }}
{{- fail (printf "nodeHealthCheck.maxUnhealthy must be a bare integer (e.g. 0, 1) or an integer percentage (e.g. 50%%), got %q" $maxUnhealthy) }}
{{- fail (printf "%s must be a bare integer (e.g. 0, 1) or an integer percentage (e.g. 50%%), got %q" $muSource $maxUnhealthy) }}
{{- end }}
nodeStartupTimeout: {{ $.Values.nodeHealthCheck.nodeStartupTimeout }}
{{- $nodeStartupTimeout := $.Values.nodeHealthCheck.nodeStartupTimeout }}
{{- if hasKey $group "nodeStartupTimeout" }}{{ $nodeStartupTimeout = $group.nodeStartupTimeout }}{{ end }}
nodeStartupTimeout: {{ $nodeStartupTimeout }}
{{- $maxUnhealthy := toString $.Values.nodeHealthCheck.maxUnhealthy }}
{{- $muSource := "nodeHealthCheck.maxUnhealthy" }}
{{- if hasKey $group "maxUnhealthy" }}
{{- $groupMaxUnhealthy := toString $group.maxUnhealthy }}
{{- if ne $groupMaxUnhealthy "" }}
{{- $maxUnhealthy = $groupMaxUnhealthy }}
{{- $muSource = printf "nodeGroups.%s.maxUnhealthy" $groupName }}
{{- end }}
{{- end }}
{{- if hasSuffix "%" $maxUnhealthy }}
{{- if not (regexMatch "^[0-9]+%$" $maxUnhealthy) }}
{{- fail (printf "%s must be a bare integer (e.g. 0, 1) or an integer percentage (e.g. 50%%), got %q" $muSource $maxUnhealthy) }}
{{- end }}
maxUnhealthy: {{ $maxUnhealthy | quote }}
{{- else if regexMatch "^[0-9]+$" $maxUnhealthy }}
maxUnhealthy: {{ $maxUnhealthy | int }}
{{- else }}
{{- fail (printf "%s must be a bare integer (e.g. 0, 1) or an integer percentage (e.g. 50%%), got %q" $muSource $maxUnhealthy) }}
{{- end }}
{{- $nodeStartupTimeout := $.Values.nodeHealthCheck.nodeStartupTimeout }}
{{- if hasKey $group "nodeStartupTimeout" }}
{{- $groupNodeStartupTimeout := toString $group.nodeStartupTimeout }}
{{- if ne $groupNodeStartupTimeout "" }}
{{- $nodeStartupTimeout = $groupNodeStartupTimeout }}
{{- end }}
{{- end }}
nodeStartupTimeout: {{ $nodeStartupTimeout }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/kubernetes/templates/cluster.yaml` around lines 680 - 698,
Empty node-group overrides are being treated as real values, which breaks
fallback to nodeHealthCheck defaults. Update the cluster template logic around
maxUnhealthy and nodeStartupTimeout so the per-group value is used only when it
is non-empty, not just when hasKey is true; keep the existing
validation/rendering paths in the same template block and preserve fallback to
$.Values.nodeHealthCheck.* for empty strings. Add a regression test covering
nodeGroups.<name>.maxUnhealthy and nodeGroups.<name>.nodeStartupTimeout set to
empty strings to confirm they inherit the global defaults.

IvanHunters
IvanHunters previously approved these changes Jul 4, 2026

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

Approve — strictly opt-in and additive.

make-generate artifacts (values.yaml, values.schema.json, README, types.go, cozyrds openAPISchema) are all in sync, the per-field fallback is correct (setting only maxUnhealthy keeps the inherited nodeStartupTimeout, no accidental null-out), and upgrade / fresh-install are a no-op.

Non-blocking, but worth resolving since all three prior reviews raised it and it's still open on the current tip:

  • cluster.yaml:682,697 use a bare hasKey, so an explicit maxUnhealthy: "" / nodeStartupTimeout: "" is treated as "set" — the former hard-fails the render, the latter emits an empty field instead of inheriting. Match the sibling storageClass guard: and (hasKey $group "x") (ne (toString $group.x) ""), plus a "" falls back test.
  • nodeStartupTimeout renders unquoted (cluster.yaml:698); a unitless numeric override becomes a YAML int and the CAPI MachineHealthCheck webhook rejects it (metav1.Duration is a string). Suggest | quote.
  • PR body is missing the required release-note fenced block.

@myasnikovdaniil
myasnikovdaniil force-pushed the feat/kubernetes-per-nodegroup-nodehealthcheck branch from eda99e7 to d9f6998 Compare July 6, 2026 05:24
IvanHunters
IvanHunters previously approved these changes Jul 6, 2026

@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

Purely additive, omitempty per-node-group overrides that fall back to the existing cluster-wide nodeHealthCheck via hasKey gating; no migration, no CRD tightening, no default flip, schema/README/openAPISchema regenerated consistently, and helm unittest passes 149/149. Two small consistency/hygiene notes below, none blocking.

Findings

[MINOR] packages/apps/kubernetes/templates/cluster.yaml:728-730 — per-group nodeStartupTimeout override has no render-time validation, unlike maxUnhealthy

The maxUnhealthy path validates the value (bare int or N%) and fails at render time with the offending source named. The sibling nodeStartupTimeout override is interpolated raw (nodeStartupTimeout: {{ $nodeStartupTimeout }}) with no format check, so a typo like nodeGroups.md0.nodeStartupTimeout: "10 minutes" is only rejected later by the CAPI MHC admission webhook (a worse failure surface than a chart-render error). This is not a regression — the cluster-wide nodeHealthCheck.nodeStartupTimeout already renders unvalidated the same way — so it is consistent with existing behaviour. Optional improvement: add a metav1.Duration-style regex check (^[0-9]+(s|m|h)$ or similar) mirroring the maxUnhealthy guard so both overrides fail fast at render with the group named.

Claim mismatches

[MISSING] release-note fenced block — CONTRIBUTING.md requires a release-note block in the PR body; the current body has What/Why/Notes prose but no fenced release-note block. Add one (e.g. feat(kubernetes): support per-node-group nodeHealthCheck maxUnhealthy / nodeStartupTimeout overrides).

Caveats

  • Existing-customer upgrade (scenario A): verified safe. Both fields are new optional string members of NodeGroup (api/apps/v1alpha1/kubernetes/types.go:277,282). The schema required list on nodeGroups.additionalProperties is unchanged (["diskSize","instanceType","maxReplicas","minReplicas"]), so existing Kubernetes CRs in customer clusters re-admit unchanged. The template gates on hasKey $group "...", so clusters with no per-group values render identically to today — no migration script needed, migrations.targetVersion correctly untouched. No default flip, no RBAC/cozyrds surface change, no image bump.
  • Fresh install (scenario B): verified safe. No new PackageSource/bundle wiring, no new .Values._cluster/_namespace keys, no cert-manager/CRD dependency, no new image references. make generate artifacts are all present and mutually consistent: types.go, values.yaml (@field annotations), values.schema.json, README.md, and the kubernetes-rd/cozyrds/kubernetes.yaml openAPISchema all carry the two new properties as optional strings.
  • Edge case (non-blocking): setting a per-group override to an empty string (maxUnhealthy: "") makes hasKey true and then fails render (empty matches no branch). To inherit the cluster-wide value the operator must omit the key entirely, which the field docs ("When unset ... applies") describe correctly. Acceptable, but worth a docs mention if this bites operators.
  • PR body says "stacked on phase1-talos-migration (#2931), retarget to main once it merges" — this is stale: the branch is already based on main and the commit sits on top of current main. No action needed, just note the body is out of date.

@myasnikovdaniil
myasnikovdaniil force-pushed the feat/kubernetes-per-nodegroup-nodehealthcheck branch from d9f6998 to 6722ca4 Compare July 8, 2026 07:40
@myasnikovdaniil
myasnikovdaniil force-pushed the feat/kubernetes-per-nodegroup-nodehealthcheck branch from 6722ca4 to 53192e4 Compare July 9, 2026 06:11
Add optional nodeGroups[name].maxUnhealthy and nodeGroups[name].nodeStartupTimeout
fields that override the cluster-wide nodeHealthCheck defaults per worker node
group. When unset, a group inherits nodeHealthCheck.{maxUnhealthy,nodeStartupTimeout},
so existing values are unaffected. The MachineHealthCheck render resolves the
per-group value first, reuses the existing int-or-string validation, and names
the offending group in the fail() message when an override is invalid.

Delivers the per-group granularity of the standalone maxUnhealthy proposal on
top of the cluster-wide nodeHealthCheck from the Talos worker bootstrap PR.

Co-authored-by: mattia-eleuteri <mattia@hidora.io>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
@myasnikovdaniil
myasnikovdaniil force-pushed the feat/kubernetes-per-nodegroup-nodehealthcheck branch from 53192e4 to db4db4a Compare July 13, 2026 07:24
@myasnikovdaniil
myasnikovdaniil merged commit eea8b73 into main Jul 13, 2026
43 checks passed
@myasnikovdaniil
myasnikovdaniil deleted the feat/kubernetes-per-nodegroup-nodehealthcheck branch July 13, 2026 10:28
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 size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants