fix(apps/kubernetes): honour nodeGroup minReplicas in MachineDeployment.spec.replicas - #3233
Conversation
…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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe 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. ChangesAutoscaler-driven MachineDeployment replicas
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
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… 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>
…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>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/apps/kubernetes/tests/nodegroups_default_test.yaml (1)
193-229: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd 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
📒 Files selected for processing (10)
api/apps/v1alpha1/kubernetes/types.gohack/e2e-apps/kubernetes-oidc-customconfig.batshack/e2e-apps/kubernetes-oidc-system.batspackages/apps/kubernetes/README.mdpackages/apps/kubernetes/templates/_helpers.tplpackages/apps/kubernetes/templates/cluster.yamlpackages/apps/kubernetes/tests/nodegroups_default_test.yamlpackages/apps/kubernetes/values.schema.jsonpackages/apps/kubernetes/values.yamlpackages/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. |
There was a problem hiding this comment.
🎯 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. |
There was a problem hiding this comment.
🎯 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.
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request 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
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 AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize 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 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
|
There was a problem hiding this comment.
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.
Kirill Ilin (sircthulhu)
left a comment
There was a problem hiding this comment.
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.replicasfrom thecluster-api-autoscaler-node-group-min-sizeannotation (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
replicason the first upgrade off the old chart), CAPI'scalculateMachineDeploymentReplicaspreserves 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.batshack/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 thecapacity.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: 0for the existingminReplicas: 0nodeGroup. Not a correctness issue.
myasnikovdaniil
left a comment
There was a problem hiding this comment.
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 → returnsminSize. So a fresh install withminReplicas: 2→replicas=2(identical to pre-PR), andminReplicas: 0→replicas=0. - Update with incoming
replicas == nil→ keeps the existing value when within[min, max], clamps into range otherwise → no worker drain onhelm 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
-
The two new OIDC test comments are inaccurate (see inline). Both state "zero MachineDeployments render", but the helper emits the default
md0whenevernodeGroupsis empty — thenodegroups_default_test.yamlcase "renders the default md0 group when nodeGroups is empty" asserts exactly that. What is actually zero is the worker DataVolumes / KubevirtMachines, becausespec.replicasis omitted and CAPI defaults it to0.kubernetes-oidc-system.batsadditionally describes the pre-PR hardcode in the present tense, andkubernetes-oidc-customconfig.batsdescribes the intermediatereplicas: {{ 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. -
Fresh-install default changes from 2 workers to 0. A default CR (
nodeGroups: {}) now comes up with no workers until the autoscaler scalesmd0from 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). -
I concur with the existing bot nit to add the max-size annotation assertion to the nonzero-
minReplicasregression case, for parity with the zero case. On theminReplicas >= 1wording: I read it as already permissive — the same sentence offers the autoscaler-from-zero alternative — so I would treat that one as optional.
| # 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, |
There was a problem hiding this comment.
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
nodeGroupsempty the helper still emits the defaultmd0(thenodegroups_default_test.yaml"renders the default md0 group when nodeGroups is empty" case asserts an MD namedtest-k8s-md0renders). What is zero is the worker DataVolumes / KubevirtMachines —spec.replicasis omitted, so CAPI's defaulting webhook seeds it to0.
Suggested framing: "the default md0 still renders, but with spec.replicas omitted CAPI seeds it to 0 on Create → no KubevirtMachine, no DataVolume.
| # AuthenticationConfiguration Secret. Combined with the chart change | ||
| # that makes `MachineDeployment.spec.replicas` honour `minReplicas`, | ||
| # dropping the field means zero MachineDeployments render and no |
There was a problem hiding this comment.
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>
Kirill Ilin (sircthulhu)
left a comment
There was a problem hiding this comment.
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-sizeand-max-sizeannotations are emitted on everyMachineDeployment, which is what lets CAPI defaultspec.replicastominSizeon 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
md0hasminReplicas: 0and the chart emits thecapacity.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.replicason 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>
Kirill Ilin (sircthulhu)
left a comment
There was a problem hiding this comment.
lgtm, thank you!
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>
Refs #3231 (parts 1 + 2 addressed here; part 3 — intra-file
cozy_wait_linstor_pool_freegate + orphandefault-chk-clickhouse-test-keeper-cluster1-0-2-0PVC cleanup — deferred to a separate follow-up PR againsthack/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.yamlhardcodedMachineDeployment.spec.replicas: 2regardless of the nodeGroup'sminReplicas. The two render-sidekubernetes-oidc-{system,customconfig}bats setminReplicas: 0and delete theirKubernetesCR without waiting for readiness — but the chart still spun up 2 × 20 GiBreplicated(DRBD) workerDataVolumes each. Their create/delete churn overlappedkubernetes-previous, which then saw slow LINSTOR provisioning (my cozyreport from run 28843794670 shows a ~6-minute gap between theKubernetesCR being created and the CDIExternalProvisioningevent 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 withcni plugin not initialized.Changes
Two edits, applied together — the chart edit is a no-op without the test edit, and vice-versa (dropping
nodeGroupsfalls back to the chart helper's defaultmd0, which itself hasminReplicas: 0, so both pieces are needed for zero workers to actually render).1.
packages/apps/kubernetes/templates/cluster.yamlspec: clusterName: {{ $.Release.Name }} - replicas: 2The field is omitted entirely so the cluster-autoscaler owns it alone. CAPI's MachineDeployment defaulting webhook seeds
spec.replicasfrom thecluster-api-autoscaler-node-group-min-sizeannotation 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 routinehelm upgradere-asserts the chart's chosen value and drains whatever the autoscaler had patched in the meantime.2.
hack/e2e-apps/kubernetes-oidc-{system,customconfig}.batsDrop the explicit
nodeGroups:block and the pairedstorageClass: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(withminReplicas: 0) renders a MachineDeployment withspec.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
No suite asserted on
MachineDeployment.spec.replicasdirectly, so nothing needed updating.helm template — verify the omit
With
tests/values/common.yaml(which hasminReplicas: 0), the rendered MD carries thecluster-api-autoscaler-node-group-min-size: "0"annotation and nospec.replicasfield. CAPI's defaulting webhook will fill inspec.replicas: 0on Create — no worker DataVolumes provisioned until scale-out.With a user CR that sets
minReplicas: 2(the shape thekubernetes-latest/kubernetes-previousbats CRs use), the rendered MD carries thecluster-api-autoscaler-node-group-min-size: "2"annotation and again nospec.replicasfield. CAPI's defaulting webhook will fill inspec.replicas: 2on 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 templateof the OIDC bats CR bodies (after this PR): 1 MachineDeployment with no chart-managedspec.replicasandmin-size: "0"on the autoscaler annotation. CAPI seedsreplicas: 0on 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-assertedMachineDeployment.spec.replicas: 2on every Helm action, so a customer running with the autoscaler at, say,replicas: 8would 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. Withspec.replicasno 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 carriesminReplicas: 0, which is what CAPI's defaulting webhook seedsspec.replicasfrom. Thevalues.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 specificaddons.ingressNginx.enabled: trueinteraction operators need to be aware of on fresh installs.Test plan
helm unittest packages/apps/kubernetes/— 171/171 passing.helm templatewithtests/values/common.yaml(minReplicas: 0) — MD.replicas: 0.helm templatewith aminReplicas: 2override — MD.replicas: 2 (byte-identical to pre-PR).kubernetes-latestunchanged (spawns its two workers as before),kubernetes-oidc-{system,customconfig}render zero workers,kubernetes-previousno longer flakes on LINSTOR pool pressure originating from the earlier OIDC suite.Notes
cozy_wait_linstor_pool_freegate between kube sub-tests and the orphan clickhouse keeper PVC cleanup) is not in this PR — worth a separate targeted change tohack/e2e-apps/run-kubernetes.sh, and it's a hardening-only improvement rather than a root-cause fix. Happy to send it as a follow-up if wanted.Release note
Summary by CodeRabbit
MachineDeploymentmanifests no longer setspec.replicas, avoiding chart upgrades overwriting autoscaler-managed sizing.nodeGroups: {}behavior: a defaultmd0is emitted withminReplicas: 0/ingress-nginxrole, and workers are provisioned only after an autoscaler-triggering unschedulable Pod or manual scaling; updated related storage and ingress guidance.