Skip to content

fix(apps/kubernetes): honour nodeGroup minReplicas in MachineDeployment.spec.replicas - #3233

Merged
IvanHunters merged 6 commits into
mainfrom
fix/oidc-render-tests-drop-nodegroups
Jul 8, 2026
Merged

fix(apps/kubernetes): honour nodeGroup minReplicas in MachineDeployment.spec.replicas#3233
IvanHunters merged 6 commits into
mainfrom
fix/oidc-render-tests-drop-nodegroups

Conversation

@IvanHunters

@IvanHunters IvanHunters commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Refs #3231 (parts 1 + 2 addressed here; part 3 — intra-file cozy_wait_linstor_pool_free gate + orphan default-chk-clickhouse-test-keeper-cluster1-0-2-0 PVC cleanup — deferred to a separate follow-up PR against hack/e2e-apps/run-kubernetes.sh).

Do not auto-close the issue on merge — the hardening work is a distinct concern and belongs on its own tracker entry.

Root cause chain (recap from the issue)

packages/apps/kubernetes/templates/cluster.yaml hardcoded MachineDeployment.spec.replicas: 2 regardless of the nodeGroup's minReplicas. The two render-side kubernetes-oidc-{system,customconfig} bats set minReplicas: 0 and delete their Kubernetes CR without waiting for readiness — but the chart still spun up 2 × 20 GiB replicated (DRBD) worker DataVolumes each. Their create/delete churn overlapped kubernetes-previous, which then saw slow LINSTOR provisioning (my cozyreport from run 28843794670 shows a ~6-minute gap between the Kubernetes CR being created and the CDI ExternalProvisioning event actually firing for the worker root disk); the tenant node registered late; Cilium's CNI init missed the 12-minute node-join deadline; and the test failed with cni plugin not initialized.

Changes

Two edits, applied together — the chart edit is a no-op without the test edit, and vice-versa (dropping nodeGroups falls back to the chart helper's default md0, which itself has minReplicas: 0, so both pieces are needed for zero workers to actually render).

1. packages/apps/kubernetes/templates/cluster.yaml

 spec:
   clusterName: {{ $.Release.Name }}
-  replicas: 2

The field is omitted entirely so the cluster-autoscaler owns it alone. CAPI's MachineDeployment defaulting webhook seeds spec.replicas from the cluster-api-autoscaler-node-group-min-size annotation on Create (documented at https://cluster-api.sigs.k8s.io/tasks/automated-machine-management/autoscaling"if it's a new MachineDeployment or MachineSet, use min size"), and Server-Side-Apply field-manager ownership then keeps chart upgrades from overwriting whatever value the autoscaler patched in the meantime.

Emitting the field from the chart at all — even with the semantically-correct {{ $group.minReplicas }} — would keep the helm-controller ↔ cluster-autoscaler field-ownership fight alive: helm-controller applies with SSA + force-conflicts (https://fluxcd.io/flux/components/helm/helmreleases/), so every routine helm upgrade re-asserts the chart's chosen value and drains whatever the autoscaler had patched in the meantime.

2. hack/e2e-apps/kubernetes-oidc-{system,customconfig}.bats

Drop the explicit nodeGroups: block and the paired storageClass: from the render-side CR bodies. The tests only assert on the KamajiControlPlane and the cozy-realm Keycloak objects — worker plumbing was never part of their coverage.

After the chart fix, the chart helper's default md0 (with minReplicas: 0) renders a MachineDeployment with spec.replicas: 0 — the resource is still emitted for API-surface completeness but never creates a KubevirtMachine, never provisions a DataVolume, and never touches DRBD.

Verification

helm-unittest

Charts:      1 passed, 1 total
Test Suites: 16 passed, 16 total
Tests:       171 passed, 171 total

No suite asserted on MachineDeployment.spec.replicas directly, so nothing needed updating.

helm template — verify the omit

With tests/values/common.yaml (which has minReplicas: 0), the rendered MD carries the cluster-api-autoscaler-node-group-min-size: "0" annotation and no spec.replicas field. CAPI's defaulting webhook will fill in spec.replicas: 0 on Create — no worker DataVolumes provisioned until scale-out.

With a user CR that sets minReplicas: 2 (the shape the kubernetes-latest / kubernetes-previous bats CRs use), the rendered MD carries the cluster-api-autoscaler-node-group-min-size: "2" annotation and again no spec.replicas field. CAPI's defaulting webhook will fill in spec.replicas: 2 on Create — byte-identical topology to the pre-PR chart's fresh-install state (2 workers), but without the recurring drain-on-upgrade cycle.

Render-side render walk

helm template of the OIDC bats CR bodies (after this PR): 1 MachineDeployment with no chart-managed spec.replicas and min-size: "0" on the autoscaler annotation. CAPI seeds replicas: 0 on Create, 0 KubevirtMachines emitted, 0 DataVolume provisioning triggered. Test assertions on KamajiControlPlane / KeycloakClient / KeycloakClientScope / AuthenticationConfiguration Secret are unchanged.

Upgrade impact

Existing customers stop losing workers on helm upgrade. Prior to this PR the chart re-asserted MachineDeployment.spec.replicas: 2 on every Helm action, so a customer running with the autoscaler at, say, replicas: 8 would see 6 workers drained on every routine Cozystack platform bump and then wait for the autoscaler to scale back up in response to unschedulable Pods. With spec.replicas no longer emitted by the chart, SSA field-manager ownership preserves the autoscaler's value across upgrades unconditionally.

Fresh installs with nodeGroups: {} (the schema default) transition from "2 workers by default" to "0 workers until scale-out". The chart helper's default md0 carries minReplicas: 0, which is what CAPI's defaulting webhook seeds spec.replicas from. The values.yaml / README / helper docstring / regenerated Go type doc + openAPISchema call this out explicitly; the release-note repeats it below. See finding 5 in the paired self-review for the specific addons.ingressNginx.enabled: true interaction operators need to be aware of on fresh installs.

Test plan

  • helm unittest packages/apps/kubernetes/ — 171/171 passing.
  • helm template with tests/values/common.yaml (minReplicas: 0) — MD.replicas: 0.
  • helm template with a minReplicas: 2 override — MD.replicas: 2 (byte-identical to pre-PR).
  • E2E: kubernetes-latest unchanged (spawns its two workers as before), kubernetes-oidc-{system,customconfig} render zero workers, kubernetes-previous no longer flakes on LINSTOR pool pressure originating from the earlier OIDC suite.

Notes

Release note

fix(apps/kubernetes): stop the chart from managing `MachineDeployment.spec.replicas`; the cluster-autoscaler owns the field alone. CAPI's defaulting webhook now seeds `spec.replicas` from the `cluster-api-autoscaler-node-group-min-size` annotation on Create, and SSA field-manager ownership keeps subsequent `helm upgrade` actions from overwriting whatever value the autoscaler patched. Two behavioural changes worth calling out:

- Existing customers: `helm upgrade` no longer drains workers on every platform bump — the autoscaler-held `spec.replicas` is preserved unconditionally. Prior to this change the chart hardcoded `replicas: 2`, which drained back to 2 on every reconcile action.
- Fresh installs with `nodeGroups: {}` (the schema default) no longer come up with two workers by default. The chart helper's default md0 carries `minReplicas: 0`, and CAPI's defaulting webhook seeds `spec.replicas` from that. Operators enabling `addons.ingressNginx.enabled: true` on such a CR must either supply an explicit nodeGroup with `roles: [ingress-nginx]` and `minReplicas >= 1`, or accept that the cluster-autoscaler will bring the default md0 up in response to the ingress-nginx controller Pods becoming Pending on install.

Summary by CodeRabbit

  • Bug Fixes
    • Rendered MachineDeployment manifests no longer set spec.replicas, avoiding chart upgrades overwriting autoscaler-managed sizing.
  • Documentation
    • Clarified nodeGroups: {} behavior: a default md0 is emitted with minReplicas: 0/ingress-nginx role, and workers are provisioned only after an autoscaler-triggering unschedulable Pod or manual scaling; updated related storage and ingress guidance.
  • Tests
    • Strengthened default node group rendering assertions and adjusted OIDC e2e manifests to reduce CI provisioning churn.

…from render-side OIDC tests

Closes #3231.

`packages/apps/kubernetes/templates/cluster.yaml` hardcoded
`MachineDeployment.spec.replicas: 2` regardless of the nodeGroup's
`minReplicas`. The two render-side `kubernetes-oidc-{system,customconfig}`
bats — which set `minReplicas: 0` and delete their `Kubernetes` CR
without waiting for readiness — still provisioned two 20 GiB
`replicated` (DRBD) worker `DataVolume`s each. Their create/delete
churn overlapped the very next test, `kubernetes-previous`, and put
transient pressure on a LINSTOR satellite's ZFS pool: worker root
disks landed late, tenant nodes registered late, Cilium's CNI init
missed the 12-minute node-join deadline, and the test failed with
`cni plugin not initialized`.

Two changes, applied together — the chart fix is a no-op without
the test fix, and the test fix is a no-op without the chart fix,
because dropping `nodeGroups` falls back to the chart helper's
default `md0` (with `minReplicas: 0`).

**Chart**: `spec.replicas: {{ $group.minReplicas }}` so the field
matches the autoscaler annotations already emitted just above.
`minReplicas: 0` groups now genuinely scale from zero and do not
silently provision workers; groups with `minReplicas >= 2` (the
kubernetes-latest / kubernetes-previous bats CRs, the tests/values/
common.yaml default, the values.yaml default that gets templated
into the helper's `md0`) render byte-identical to before.

**Render-side OIDC tests**: drop the explicit `nodeGroups:` block
and `storageClass:` — falling back to the chart helper's default
`md0` (now with an effective `replicas: 0` after the chart change)
means the MachineDeployment renders but never creates a KubevirtMachine
and no DataVolume is provisioned. The tests only assert on the
KamajiControlPlane and the cozy-realm Keycloak objects, so removing
worker plumbing does not narrow their coverage.

Upgrade impact: any existing customer CR that already carries
`nodeGroups.md0.minReplicas: 0` (rare — the default in tests/values
and the chart helper both use it, but real user CRs almost always
have `minReplicas >= 1`) will see the MD's `spec.replicas` reconcile
from `2` down to `0` on the next chart apply. If the cluster-
autoscaler is enabled it will scale back up as soon as an
unschedulable pod appears; if it is not, the group intentionally
becomes empty (which is what `minReplicas: 0` requests literally).
This matches the semantics the autoscaler annotations were already
advertising — the hardcoded `2` was contradicting them.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: ece98635-2010-4a06-a823-34dd89c1f1ce

📥 Commits

Reviewing files that changed from the base of the PR and between e23d89d and 6f5d826.

📒 Files selected for processing (2)
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/tests/nodegroups_default_test.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/tests/nodegroups_default_test.yaml

📝 Walkthrough

Walkthrough

The chart now leaves MachineDeployment replicas unset so autoscaler behavior controls worker count. Related docs and schema text describe the deferred default node group, render tests assert replicas are omitted, and e2e manifests no longer request worker or storage provisioning.

Changes

Autoscaler-driven MachineDeployment replicas

Layer / File(s) Summary
MachineDeployment replicas ownership by autoscaler
packages/apps/kubernetes/templates/cluster.yaml, packages/apps/kubernetes/templates/_helpers.tpl
Removes fixed spec.replicas from the MachineDeployment template and expands helper documentation on minReplicas: 0 behavior.
Regression tests for replicas omission
packages/apps/kubernetes/tests/nodegroups_default_test.yaml
Adds cases asserting spec.replicas is omitted for minReplicas: 0 and minReplicas: 2, and checks cluster-autoscaler sizing annotations.
Documentation and schema updates for deferred provisioning
api/apps/v1alpha1/kubernetes/types.go, packages/apps/kubernetes/README.md, packages/apps/kubernetes/values.yaml, packages/apps/kubernetes/values.schema.json, packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
Updates API comments, README text, values docs, JSON schema, and CRD schema descriptions to describe the default md0 node group with deferred worker provisioning.
E2E test manifest simplification
hack/e2e-apps/kubernetes-oidc-customconfig.bats, hack/e2e-apps/kubernetes-oidc-system.bats
Removes nodeGroups and storageClass from applied CR manifests and replaces them with comments about render-side validation.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Helm as Helm Chart
  participant CAPI as MachineDeployment
  participant CA as Cluster Autoscaler

  Helm->>CAPI: Render MachineDeployment without spec.replicas
  CA->>CAPI: Observe pending unschedulable Pod
  CA->>CAPI: Set replica count from autoscaler annotations
  Helm-->>CAPI: Subsequent upgrades leave replicas untouched
Loading

Suggested reviewers: kvaps, lllamnyp, androndo

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #39 asks for a Cilium apiserver endpoint override in Cozystack ConfigMap, but this PR only changes kubernetes nodeGroup/replica docs, templates, and tests. Implement the ConfigMap override for Cilium's kube-apiserver endpoint, or split these unrelated kubernetes chart changes into a separate PR.
Out of Scope Changes check ⚠️ Warning These changes focus on nodeGroup defaults, autoscaler behavior, and test/docs cleanup, which are unrelated to issue #39's Cilium endpoint override. Remove the unrelated kubernetes chart/docs/test edits from this PR, or separate them from the Cilium ConfigMap work.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: omitting MachineDeployment.spec.replicas so node-group replica behavior follows autoscaler/defaulting.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/oidc-render-tests-drop-nodegroups

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.

@github-actions github-actions Bot added size/M This PR changes 30-99 lines, ignoring generated files area/kubernetes Issues or PRs related to the tenant Kubernetes app kind/bug Categorizes issue or PR as related to a bug labels Jul 7, 2026
… zero-worker topology + regression-guard the replicas fix

Follow-ups from the self-review of #3233:

- `values.yaml` / helper docstring / regenerated README + schema +
  cozyrds openAPISchema + Go type doc: the previous "one
  ingress-nginx worker" contract was already misleading (helper
  default carried `minReplicas: 0` but the chart hardcoded
  `spec.replicas: 2`), and the replicas fix makes the docstring
  strictly false — an empty-nodeGroups install now provisions zero
  workers until either the cluster-autoscaler responds to an
  unschedulable Pod or the operator explicitly scales the group.
  Update the docstrings to describe the actual behaviour and to
  spell out the ingress-nginx-addon implication so operators are
  not surprised at addon-install time.

- `tests/nodegroups_default_test.yaml`: two regression cases pinning
  `MachineDeployment.spec.replicas == $group.minReplicas` for
  `minReplicas: 0` (the render-side OIDC bats path — zero worker
  DataVolumes provisioned) and for `minReplicas: 2` (the
  kubernetes-latest / kubernetes-previous bats path — byte-
  identical to pre-PR behaviour). Without these a future refactor
  can silently re-hardcode `replicas: 2` and turn no tests red.

  helm-unittest suite grows from 171 to 173, both new cases pass.

Ref: #3231.
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files and removed size/M This PR changes 30-99 lines, ignoring generated files labels Jul 7, 2026
…oscaler owns the field alone

Structural follow-up to the previous commit's `replicas: {{ $group.minReplicas }}`.
Emitting the field from the chart at all — even with the semantically-
correct value — keeps the helm-controller ↔ cluster-autoscaler field-
ownership fight alive: helm-controller applies with SSA + force-conflicts
(https://fluxcd.io/flux/components/helm/helmreleases/), so every routine
`helm upgrade` action re-asserts `replicas: {{ $group.minReplicas }}` and
drains whatever the autoscaler had patched in the meantime, with a real
disruption window on running clusters.

Drop the field from the manifest entirely. CAPI's MachineDeployment
defaulting webhook seeds `spec.replicas` from the
`cluster-api-autoscaler-node-group-min-size` annotation on Create (see
https://cluster-api.sigs.k8s.io/tasks/automated-machine-management/autoscaling),
so fresh installs still start with the intended minimum. From that
point on the autoscaler owns the field via SSA field-manager tracking
and chart apply never touches it again.

Behavioural summary:
  * Fresh install with `minReplicas: 0` → CAPI defaults `replicas: 0`
    on Create → no worker DataVolumes provisioned until scale-out.
  * Fresh install with `minReplicas: 2` → CAPI defaults `replicas: 2`
    on Create → byte-identical to pre-#3231 behaviour.
  * Existing customer (any minReplicas) → next chart upgrade does not
    touch spec.replicas — the autoscaler-held value is preserved
    unconditionally.

Regression tests updated to assert:
  * `notExists: spec.replicas` on the emitted MachineDeployment (the
    exact contract this commit establishes).
  * Autoscaler `min-size` / `max-size` annotations continue to carry
    the nodeGroup's declared bounds (so a future refactor cannot
    accidentally drop the wiring the defaulting webhook depends on).

helm-unittest suite stays at 173/173.

Ref: #3231.
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>

@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

🧹 Nitpick comments (1)
packages/apps/kubernetes/tests/nodegroups_default_test.yaml (1)

193-229: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add max-size annotation assertion for parity with the zero-minReplicas case.

The zero-minReplicas test (Lines 178-191) asserts both min-size and max-size annotations, but this nonzero-minReplicas case only asserts min-size. Adding the max-size check closes a small coverage gap and keeps the two regression cases symmetric.

♻️ Proposed addition
       - equal:
           path: metadata.annotations["cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size"]
           value: "2"
         documentSelector:
           path: kind
           value: MachineDeployment
           matchMany: true
+      - equal:
+          path: metadata.annotations["cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size"]
+          value: "10"
+        documentSelector:
+          path: kind
+          value: MachineDeployment
+          matchMany: true
🤖 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/tests/nodegroups_default_test.yaml` around lines 193
- 229, The MachineDeployment regression test for the nonzero-minReplicas case is
missing the max-size annotation check that the zero-minReplicas case already
covers. Update the existing test block for nodeGroups.worker0 so it also asserts
metadata.annotations["cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size"]
equals "10", keeping parity with the neighboring zero-minReplicas assertion set
and using the existing MachineDeployment documentSelector.
🤖 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 `@api/apps/v1alpha1/kubernetes/types.go`:
- Line 25: The comment in the worker nodes configuration doc is too strict about
custom ingress-nginx node groups needing minReplicas >= 1. Reword the guidance
around the nodeGroups default behavior so it says that if you want immediate
workers you should set a custom group with roles: [ingress-nginx] and
minReplicas >= 1, but that minReplicas can still be 0 and the autoscaler will
scale it when the ingress-nginx controller Pods are pending. Keep the reference
centered on the Worker nodes configuration map and the default md0 behavior.

In `@packages/apps/kubernetes/values.yaml`:
- Line 92: The nodeGroups documentation in values.yaml is too restrictive by
stating that an ingress-nginx node group must have minReplicas >= 1; update the
wording to stay permissive and accurate. In the nodeGroups description, and any
related generated text driven by it, clarify that a custom group with roles:
[ingress-nginx] may also start at 0 and be scaled by the cluster-autoscaler when
the controller Pods are Pending, while keeping the guidance about the default
md0 and custom groups intact.

---

Nitpick comments:
In `@packages/apps/kubernetes/tests/nodegroups_default_test.yaml`:
- Around line 193-229: The MachineDeployment regression test for the
nonzero-minReplicas case is missing the max-size annotation check that the
zero-minReplicas case already covers. Update the existing test block for
nodeGroups.worker0 so it also asserts
metadata.annotations["cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size"]
equals "10", keeping parity with the neighboring zero-minReplicas assertion set
and using the existing MachineDeployment documentSelector.
🪄 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: 5ceec56e-f91f-4bb8-9b32-7782ee990120

📥 Commits

Reviewing files that changed from the base of the PR and between e60ec04 and b3aac80.

📒 Files selected for processing (10)
  • api/apps/v1alpha1/kubernetes/types.go
  • hack/e2e-apps/kubernetes-oidc-customconfig.bats
  • hack/e2e-apps/kubernetes-oidc-system.bats
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/templates/_helpers.tpl
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/tests/nodegroups_default_test.yaml
  • packages/apps/kubernetes/values.schema.json
  • packages/apps/kubernetes/values.yaml
  • packages/system/kubernetes-rd/cozyrds/kubernetes.yaml

// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="storageClass is immutable"
StorageClass string `json:"storageClass"`
// Worker nodes configuration map. When left empty, a single default node group `md0` (one `ingress-nginx` worker) is provisioned. Provide your own groups to take full control — they are not merged with the default, so you may name and omit groups freely.
// Worker nodes configuration map. When left empty, a default node group `md0` is emitted with `minReplicas: 0` and `roles: [ingress-nginx]` — the MachineDeployment renders but provisions no workers until an unschedulable Pod triggers the cluster-autoscaler (or an operator scales the group manually). Enabling `addons.ingressNginx.enabled: true` on a CR with default `nodeGroups: {}` therefore requires either supplying a nodeGroup with `roles: [ingress-nginx]` and `minReplicas >= 1`, or waiting for the autoscaler to bring up the default md0 in response to the ingress-nginx controller Pods becoming Pending. Provide your own groups to take full control — they are not merged with the default, so you may name and omit groups freely.

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 | 🟡 Minor | ⚡ Quick win

Relax the minReplicas requirement.

minReplicas >= 1 is stricter than the chart behavior here; a custom ingress-nginx node group can still start at 0 and be scaled by the autoscaler once the controller Pods are pending. Please reword this as an “immediate workers” recommendation, not a hard requirement.

🤖 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 `@api/apps/v1alpha1/kubernetes/types.go` at line 25, The comment in the worker
nodes configuration doc is too strict about custom ingress-nginx node groups
needing minReplicas >= 1. Reword the guidance around the nodeGroups default
behavior so it says that if you want immediate workers you should set a custom
group with roles: [ingress-nginx] and minReplicas >= 1, but that minReplicas can
still be 0 and the autoscaler will scale it when the ingress-nginx controller
Pods are pending. Keep the reference centered on the Worker nodes configuration
map and the default md0 behavior.

## @field {Kubelet} [kubelet] - Kubelet resource reservations for this node group.

## @param {map[string]NodeGroup} nodeGroups - Worker nodes configuration map. When left empty, a single default node group `md0` (one `ingress-nginx` worker) is provisioned. Provide your own groups to take full control — they are not merged with the default, so you may name and omit groups freely.
## @param {map[string]NodeGroup} nodeGroups - Worker nodes configuration map. When left empty, a default node group `md0` is emitted with `minReplicas: 0` and `roles: [ingress-nginx]` — the MachineDeployment renders but provisions no workers until an unschedulable Pod triggers the cluster-autoscaler (or an operator scales the group manually). Enabling `addons.ingressNginx.enabled: true` on a CR with default `nodeGroups: {}` therefore requires either supplying a nodeGroup with `roles: [ingress-nginx]` and `minReplicas >= 1`, or waiting for the autoscaler to bring up the default md0 in response to the ingress-nginx controller Pods becoming Pending. Provide your own groups to take full control — they are not merged with the default, so you may name and omit groups freely.

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 | 🟡 Minor | ⚡ Quick win

Keep the source wording permissive.

This file feeds the generated README/schema/CRD text, so the same minReplicas >= 1 overstatement will propagate everywhere. A custom ingress-nginx node group can also be left at 0 and autoscaled from pending controller Pods.

🤖 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/values.yaml` at line 92, The nodeGroups
documentation in values.yaml is too restrictive by stating that an ingress-nginx
node group must have minReplicas >= 1; update the wording to stay permissive and
accurate. In the nodeGroups description, and any related generated text driven
by it, clarify that a custom group with roles: [ingress-nginx] may also start at
0 and be scaled by the cluster-autoscaler when the controller Pods are Pending,
while keeping the guidance about the default md0 and custom groups intact.

@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 improves cluster resource management by removing the hardcoded spec.replicas field in the MachineDeployment template. By delegating replica management to the cluster-autoscaler, the chart avoids disruptive worker drains during routine upgrades and ensures that worker provisioning aligns with actual demand.

Highlights

  • MachineDeployment Replicas Management: Removed hardcoded spec.replicas from the MachineDeployment template to allow the cluster-autoscaler to manage replica counts, preventing unnecessary worker drains during Helm upgrades.
  • Test Optimization: Removed explicit nodeGroups from OIDC E2E tests to eliminate redundant worker provisioning and storage churn during CI runs.
  • Documentation Updates: Updated API types, README, and schema documentation to reflect the new default behavior where fresh installs provision zero workers until triggered by the autoscaler.
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.

@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 updates the Kubernetes application package to omit spec.replicas from the MachineDeployment template, allowing the cluster-autoscaler to solely own and manage the replica count. This prevents worker nodes from being drained during routine Helm upgrades. Consequently, the default nodeGroups configuration now defaults to minReplicas: 0, meaning no workers are provisioned initially until triggered by the autoscaler or scaled manually. Documentation, schemas, and helper templates have been updated to reflect this behavior, and regression tests have been added to verify that spec.replicas is omitted and autoscaler annotations are correctly set. Additionally, E2E tests have been updated to omit nodeGroups to prevent storage churn and test flakes. No review comments were provided, so there is no feedback to evaluate.

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.

@sircthulhu Kirill Ilin (sircthulhu) 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.

The functional change is correct — removing the hardcoded MachineDeployment.spec.replicas: 2 and letting the field be owned by the cluster-autoscaler is the standard CAPI + autoscaler pattern, and it's safe in both paths:

  • Fresh install (Create): CAPI's MachineDeployment defaulting webhook seeds spec.replicas from the cluster-api-autoscaler-node-group-min-size annotation (already emitted by the chart, unchanged here).
  • Existing cluster (Update): even under Helm 3's client-side three-way merge (which will generate a patch removing replicas on the first upgrade off the old chart), CAPI's calculateMachineDeploymentReplicas preserves the previous live value (oldMD.Spec.Replicas) when the incoming value is nil on an update, so a cluster the autoscaler holds above min-size is not drained. The real guarantee is the CAPI webhook's update path rather than SSA field ownership, but the outcome is correct either way.

The generated artifacts and the new helm-unittest regression guard are all consistent and well-targeted.

Two comment fixes are needed before merge:

1. Both bats comments claim "zero MachineDeployments render" — this is inaccurate.

When nodeGroups is empty, templates/_helpers.tpl (kubernetes.nodeGroups) emits a default md0 group, so exactly one MachineDeployment renders (default md0 with minReplicas: 0). What is zero is the number of KubevirtMachines / worker DataVolumes, because CAPI seeds replicas to 0 — which matches the PR description. Affects:

  • hack/e2e-apps/kubernetes-oidc-customconfig.bats
  • hack/e2e-apps/kubernetes-oidc-system.bats

2. kubernetes-oidc-system.bats describes the bug as still present in the PR that fixes it.

The comment states the chart "hardcodes spec.replicas: 2 ... (see #3231)", but this PR removes that hardcoding, so the statement is false post-merge — and it contradicts the sibling customconfig comment, which correctly references the post-fix behavior. Please rewrite it to describe the post-fix rationale (default md0, minReplicas: 0, CAPI seeds replicas: 0, no DataVolumes), matching the customconfig block.

Non-blocking notes:

  • Fresh installs with the default nodeGroups: {} now come up with 0 workers instead of 2, relying on the autoscaler to scale from zero. This is well-documented and scale-from-zero is wired via the capacity.cluster-autoscaler.kubernetes.io/{cpu,memory} annotations — flagging only as an intentional behavior change worth confirming.
  • The PR body's claim that the chart edit and the bats edit are each "a no-op without the other" isn't strictly accurate — the chart fix alone would already render replicas: 0 for the existing minReplicas: 0 nodeGroup. Not a correctness issue.

myasnikovdaniil
myasnikovdaniil previously approved these changes Jul 8, 2026

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

LGTM — the core mechanism is verified against CAPI v1.10.1 and the chart's own unittest, and both the real-cluster e2e and the helm upgrade path are preserved. A few non-blocking follow-ups below.

Business context: The chart hardcoded MachineDeployment.spec.replicas: 2, defeating a nodeGroup's minReplicas (especially minReplicas: 0). That forced the render-side kubernetes-oidc-* e2e tests to provision two 20 GiB DRBD worker DataVolumes each, whose create/delete churn overlapped kubernetes-previous and flaked it under LINSTOR pool pressure (#3231, parts 1+2).

Verification

The PR pivots to omitting spec.replicas entirely and relying on CAPI's defaulting webhook. I confirmed this against cluster-api v1.10.1 (the pinned cluster-api-controller), function calculateMachineDeploymentReplicas:

  • New MachineDeployment (oldMD == nil) with both autoscaler annotations present → returns minSize. So a fresh install with minReplicas: 2replicas=2 (identical to pre-PR), and minReplicas: 0replicas=0.
  • Update with incoming replicas == nil → keeps the existing value when within [min, max], clamps into range otherwise → no worker drain on helm upgrade.
  • Requires both the min-size and max-size annotations, otherwise it defaults to 1. The template emits both unconditionally and the annotation keys match the CAPI constants exactly.

Also confirmed: the per-tenant cluster-autoscaler is deployed by default (gated on _namespace.etcd) with --enforce-node-group-min-size=true and the capacity.cluster-autoscaler.kubernetes.io/{cpu,memory} annotations, so scale-from-zero is wired; run-kubernetes.sh sets minReplicas: 2 and its .status.replicas=2 / readyReplicas=2 waits still hold; helm unittest passes 173/173.

Non-blocking follow-ups

  1. The two new OIDC test comments are inaccurate (see inline). Both state "zero MachineDeployments render", but the helper emits the default md0 whenever nodeGroups is empty — the nodegroups_default_test.yaml case "renders the default md0 group when nodeGroups is empty" asserts exactly that. What is actually zero is the worker DataVolumes / KubevirtMachines, because spec.replicas is omitted and CAPI defaults it to 0. kubernetes-oidc-system.bats additionally describes the pre-PR hardcode in the present tense, and kubernetes-oidc-customconfig.bats describes the intermediate replicas: {{ minReplicas }} approach that the final commit superseded by omitting the field. Since aligning docs with actual behavior is the stated purpose of the docs commit, worth correcting.

  2. Fresh-install default changes from 2 workers to 0. A default CR (nodeGroups: {}) now comes up with no workers until the autoscaler scales md0 from zero on the first pending Pod. This is intentional and documented across values / README / schema / CRD / Go doc and the release note — flagging only for an explicit maintainer ack, since it is a user-visible default change and scale-from-zero on a brand-new cluster is now a load-bearing path that previously never executed (clusters always started at 2).

  3. I concur with the existing bot nit to add the max-size annotation assertion to the nonzero-minReplicas regression case, for parity with the zero case. On the minReplicas >= 1 wording: I read it as already permissive — the same sentence offers the autoscaler-from-zero alternative — so I would treat that one as optional.

Comment on lines +62 to +68
# objects. The chart's MachineDeployment hardcodes spec.replicas: 2
# even when a nodeGroup declares minReplicas: 0 (see #3231), so
# keeping a worker nodeGroup here provisions two 20 GiB DRBD
# DataVolumes on each `kubectl apply` and their churn overlaps
# kubernetes-previous, flaking that test with LINSTOR pool pressure.
# Dropping the field lets the schema default `{}` apply — zero
# MachineDeployments rendered, zero worker DataVolumes touched,

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 comment no longer matches the post-PR behavior:

  • Line 62 states the chart "hardcodes spec.replicas: 2 ... even when a nodeGroup declares minReplicas: 0" in the present tense — that is precisely the behavior this PR removes.
  • "zero MachineDeployments rendered" is also not accurate: with nodeGroups empty the helper still emits the default md0 (the nodegroups_default_test.yaml "renders the default md0 group when nodeGroups is empty" case asserts an MD named test-k8s-md0 renders). What is zero is the worker DataVolumes / KubevirtMachines — spec.replicas is omitted, so CAPI's defaulting webhook seeds it to 0.

Suggested framing: "the default md0 still renders, but with spec.replicas omitted CAPI seeds it to 0 on Create → no KubevirtMachine, no DataVolume.

Comment on lines +54 to +56
# AuthenticationConfiguration Secret. Combined with the chart change
# that makes `MachineDeployment.spec.replicas` honour `minReplicas`,
# dropping the field means zero MachineDeployments render and no

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.

Same "zero MachineDeployments render" inaccuracy as the system.bats comment: the default md0 still renders when nodeGroups is empty (asserted by nodegroups_default_test.yaml) — it is the worker DataVolumes that are zero, because spec.replicas is omitted and CAPI defaults it to 0.

Also "the chart change that makes MachineDeployment.spec.replicas honour minReplicas" describes the intermediate replicas: {{ minReplicas }} approach; the final chart omits the field entirely and lets CAPI's webhook seed it from the min-size annotation.

Both nodeGroups-omitted comments overstated the outcome as "zero
MachineDeployments render", but the chart helper (kubernetes.nodeGroups)
emits a default md0 group whenever the caller's nodeGroups map is
empty, so exactly one MachineDeployment renders. What is actually
zero is the number of KubevirtMachines / worker DataVolumes, because
the chart no longer manages spec.replicas and CAPI's defaulting
webhook seeds it to 0 from the autoscaler min-size annotation.

kubernetes-oidc-system.bats additionally described the bug in the
present tense ("chart hardcodes spec.replicas: 2 ... see #3231"),
which contradicts the sibling customconfig comment and would read
as an outstanding defect post-merge — rewrite it around the post-fix
rationale to match customconfig's shape.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
…ro-minReplicas case

Symmetry with the zero-minReplicas neighbour, which already covers
both min-size and max-size. CAPI's calculateMachineDeploymentReplicas
defaults to 1 (not to minSize) if either annotation is missing on a
new MachineDeployment, so both keys are load-bearing on the
Create-path guarantee and worth asserting in both regression cases.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>

@sircthulhu Kirill Ilin (sircthulhu) 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.

LGTM. The comment fixes are in and the change is correct, safe, and well-tested — I traced the full mechanism:

  • Both cluster-api-autoscaler-node-group-min-size and -max-size annotations are emitted on every MachineDeployment, which is what lets CAPI default spec.replicas to minSize on Create.
  • The cluster-autoscaler is always deployed with --enforce-node-group-min-size=true, so min-size is actively enforced, not just a create-time default.
  • Scale-from-zero works: the default md0 has minReplicas: 0 and the chart emits the capacity.cluster-autoscaler.kubernetes.io/{cpu,memory} hints the autoscaler needs to scale an empty group up for a pending Pod.
  • No drain on upgrade: the field is simply absent from the rendered manifest and CAPI's defaulting webhook preserves the live spec.replicas on UPDATE.

Worth noting the old replicas: 2 on a minReplicas: 0 group was itself a flap — the autoscaler drove the idle group back to 0, then every helm upgrade reset it to 2. This PR removes that oscillation; 0 was already the steady state, so "0 workers by default" is not a regression.

Non-blocking, but worth correcting: the cluster.yaml comment (and the paired _helpers.tpl and .bats comments) attribute upgrade-safety to "Server-Side-Apply field-manager ownership". That's the wrong mechanism — this chart is delivered as a HelmRelease via Flux helm-controller, which applies through the Helm SDK three-way merge, not SSA field managers. The value actually survives upgrades because (a) the field is absent from the rendered manifest and (b) CAPI's defaulting webhook preserves the existing live value on UPDATE. The conclusion is right, but the stated reason could send a future maintainer down the wrong path when debugging drift/ownership. Suggest rewording to reference CAPI update-time defaulting + the field being unmanaged by the chart, and dropping the SSA claim.

…API update-time defaulting

sircthulhu's post-approval note on PR#3233: both the cluster.yaml
comment and the nodegroups_default_test.yaml comment attribute the
upgrade-safety guarantee to Server-Side-Apply field-manager
ownership, but this chart ships as a HelmRelease through Flux
helm-controller, which applies via the Helm SDK three-way merge —
not SSA. The stated conclusion is correct but for the wrong reason
and would send a future maintainer chasing a non-existent field-
manager fight when debugging drift.

Rewrite both comments around the two independent CAPI mechanisms
that actually carry the guarantee:

  1. Create — the MachineDeployment defaulting webhook seeds
     spec.replicas from the min-size autoscaler annotation.
  2. Update — calculateMachineDeploymentReplicas preserves the
     existing live value whenever the incoming spec has
     replicas: nil (clamping into [min, max] on drift).

The chart guarantee reduces to "leave the field out of the rendered
manifest and let CAPI's update-time defaulting keep the live value
stable" — no field-manager ownership involved.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>

@sircthulhu Kirill Ilin (sircthulhu) 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.

lgtm, thank you!

@IvanHunters
IvanHunters merged commit 0c7b598 into main Jul 8, 2026
75 of 77 checks passed
@IvanHunters
IvanHunters deleted the fix/oidc-render-tests-drop-nodegroups branch July 8, 2026 15:58
myasnikovdaniil added a commit that referenced this pull request Jul 9, 2026
Main's #3233 landed mid-reconciliation and touched the two OIDC bats
again: nodeGroups (and storageClass) dropped from the render-side test
CRs — the schema default {} now renders one zero-replica
MachineDeployment via CAPI defaulting, so the tests provision zero
worker DataVolumes (#3231). Resolve the modify/delete as deleted and
mirror the fixture change (and its rationale) into the ported
kubernetes-oidc-system / kubernetes-oidc-customconfig Chainsaw suites.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
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/bug Categorizes issue or PR as related to a bug 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