Skip to content

fix(kubernetes): make the default md0 node group removable - #2936

Merged
Aleksei Sviridkin (lexfrei) merged 6 commits into
mainfrom
fix/kubernetes-md0-removable
Jun 30, 2026
Merged

fix(kubernetes): make the default md0 node group removable#2936
Aleksei Sviridkin (lexfrei) merged 6 commits into
mainfrom
fix/kubernetes-md0-removable

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Same-repo mirror of #2753 by mattia-eleuteri, opened so the Build CI job can run. Fork PRs skip the OCIR registry login (if: !github.event.pull_request.head.repo.fork in pull-requests.yaml) and so can't push the per-PR images the build needs — meaning a fork PR can never pass Build, regardless of the change. The commits here are unchanged and authored by mattia-eleuteri; full description and discussion in #2753.

This includes migration 44 (renumbered from 43 after the seaweedfs-db migration landed on main) and bumps targetVersion to 45.

Supersedes #2753.

fix(kubernetes): the default `md0` node group is now applied only when no node groups are configured, so it can be removed or replaced. Migration 44 pins `md0` explicitly on existing clusters to preserve their topology.

Summary by CodeRabbit

  • New Features

    • Empty nodeGroups settings now automatically provision a default md0 worker group.
    • Existing environments are updated to keep this default group available during upgrades.
  • Bug Fixes

    • User-defined node groups now fully replace the default instead of being merged with it.
    • Improved handling of node-group resources and permissions in Kubernetes app rendering.
  • Documentation

    • Updated Kubernetes app docs and schema details to reflect the new default behavior.
  • Tests

    • Added coverage for default and custom node-group configurations, plus migration behavior.

@github-actions github-actions Bot added the size/L This PR changes 100-499 lines, ignoring generated files label Jun 16, 2026
@dosubot dosubot Bot added area/kubernetes Issues or PRs related to the tenant Kubernetes app area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) backport Should change be backported on previous release kind/bug Categorizes issue or PR as related to a bug labels Jun 16, 2026
@coderabbitai

coderabbitai Bot commented Jun 16, 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
📝 Walkthrough

Walkthrough

The nodeGroups default changes from a hardcoded md0 object to an empty map {} across the API type, chart values, JSON schema, and ApplicationDefinition CRD. A new kubernetes.nodeGroups Helm helper supplies the fallback md0 configuration when the value is empty. A migration script (47→48) backfills spec.nodeGroups.md0 into existing Kubernetes CRs with fail-closed error handling.

Changes

NodeGroups default refactor and md0 migration

Layer / File(s) Summary
Default contract and schema updates
api/apps/v1alpha1/kubernetes/types.go, packages/apps/kubernetes/values.yaml, packages/apps/kubernetes/values.schema.json, packages/system/kubernetes-rd/cozyrds/kubernetes.yaml, packages/apps/kubernetes/README.md
nodeGroups default changes to {} in the kubebuilder marker, chart values, JSON schema, ApplicationDefinition schema, and README; all descriptions updated to document the empty-default md0 provisioning and no-merge behavior.
Helm helper and template consumers
packages/apps/kubernetes/templates/_helpers.tpl, packages/apps/kubernetes/templates/cluster.yaml, packages/apps/kubernetes/templates/dashboard-resourcemap.yaml
New kubernetes.nodeGroups helper renders user-defined groups or a built-in md0 default; cluster.yaml and dashboard-resourcemap.yaml now iterate over the parsed helper output instead of raw .Values.nodeGroups.
Helm tests and fixture updates
packages/apps/kubernetes/tests/nodegroups_default_test.yaml, packages/apps/kubernetes/tests/values-ci.yaml, packages/apps/kubernetes/tests/values/common.yaml, hack/e2e-apps/run-kubernetes.sh
New Helm test suite covers empty-default (md0 rendered as MachineDeployment) and user-override (no md0 leak) cases with a fake kubernetesProvider; CI values and common values define explicit md0 settings; e2e manifest adds resources: {}.
Migration 47→48: backfill md0 into existing CRs
packages/core/platform/images/migrations/migrations/47, packages/core/platform/values.yaml
Migration script enumerates all kuberneteses.apps.cozystack.io CRs, conditionally injects spec.nodeGroups.md0 via merge-patch, enforces fail-closed behavior, supports MIGRATION_DRY_RUN, then stamps version to 48; platform targetVersion bumped to 48.
Migration Bats tests
hack/kubernetes-md0-migration.bats
Bats tests verify migration 47 fail-closed behavior on read failure, patch failure, and clean success using a mock kubectl controlled by MOCK_FAIL.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • cozystack/cozystack#2980: Introduces the shared stamp_cozystack_version helper that migration 47 calls to advance the platform version stamp to 48.
  • cozystack/cozystack#3118: Updates earlier migrations (45/46) to use the same stamp_cozystack_version helper pattern used by the new migration 47.

Suggested reviewers

  • kvaps
  • lllamnyp
  • androndo

🐇 The default was hidden, baked deep in the map,
Now {} stands empty — a clean, tidy gap.
Migration rides through every namespace in turn,
Pins md0 in place so no cluster need churn.
The helper steps in when your values say naught,
A sensible default, precisely well-taught! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: default md0 is now only applied when nodeGroups is empty, making it removable or replaceable.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/kubernetes-md0-removable

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

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 refactors the Kubernetes application chart to make the default md0 node group optional and configurable. By moving the default logic from the static values file to a template helper, it prevents the default group from being automatically re-added during Helm merges. This change improves cluster flexibility and includes a migration script to ensure existing clusters maintain their current node configuration without disruption.

Highlights

  • Default Node Group Configuration: The default md0 node group is no longer hardcoded in values.yaml, allowing it to be removed or replaced by users. It is now applied dynamically via a template helper only when no other node groups are configured.
  • Migration 44: A new migration (44) has been added to explicitly pin the md0 node group on existing clusters, ensuring that current topologies are preserved during the transition to the new dynamic configuration.
  • Helm Chart Flexibility: User-defined node groups are now authoritative and are no longer merged with the default md0 group, providing users with full control over their cluster topology.
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 chart to change the default value of nodeGroups to an empty map ({}). The default md0 node group is now dynamically applied in the templates via a new helper (kubernetes.nodeGroups) only when no user-defined node groups are provided, preventing Helm merge issues. To support existing clusters and prevent their md0 node groups from being pruned on upgrade, a new platform migration script (Migration 44 -> 45) has been added to explicitly pin md0 on active clusters. The review feedback suggests minor shell script improvements in the migration script, specifically using the --patch flag for kubectl patch instead of piping to /dev/stdin, and using the more idiomatic -f - flag for kubectl apply.

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 +73 to +74
if kubectl_err=$(printf '%s' "$patch" | kubectl patch kuberneteses.apps.cozystack.io \
--namespace "$ns" "$name" --type merge --patch-file /dev/stdin 2>&1 >/dev/null); then

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

Using --patch "$patch" is more standard and robust than piping the patch via stdin and using /dev/stdin, which might not be available or behave consistently in all minimal container environments.

Suggested change
if kubectl_err=$(printf '%s' "$patch" | kubectl patch kuberneteses.apps.cozystack.io \
--namespace "$ns" "$name" --type merge --patch-file /dev/stdin 2>&1 >/dev/null); then
if kubectl_err=$(kubectl patch kuberneteses.apps.cozystack.io \
--namespace "$ns" "$name" --type merge --patch "$patch" 2>&1 >/dev/null); then

Comment on lines +102 to +104
kubectl create configmap --namespace cozy-system cozystack-version \
--from-literal version=45 --dry-run=client --output yaml \
| kubectl apply --filename -

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

Using -f - is more idiomatic and consistent with the rest of the codebase than --filename -.

Suggested change
kubectl create configmap --namespace cozy-system cozystack-version \
--from-literal version=45 --dry-run=client --output yaml \
| kubectl apply --filename -
kubectl create configmap --namespace cozy-system cozystack-version \
--from-literal version=45 --dry-run=client --output yaml \
| kubectl apply -f -

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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/core/platform/images/migrations/migrations/44`:
- Line 65: The current jq filter in the patch variable assignment generates a
merge patch containing the entire pre-read .spec.nodeGroups map, which can
overwrite concurrent edits to other node groups made by other actors between the
read and patch operations. Modify the jq filter to extract and patch only the
md0 node group from .spec.nodeGroups instead of the entire nodeGroups map. This
ensures the patch operation is minimal and only adds or updates md0 without
risking loss of concurrent modifications to other node groups.
- Around line 55-56: The current kubectl command silently suppresses all errors
with 2>/dev/null and || true, which means transient API or RBAC failures are
indistinguishable from a successfully deleted object. This allows the migration
to continue to version 45 without properly pinning md0 when the actual failure
cause was a transient issue. You need to separate the kubectl exit code from the
output check: capture the kubectl output in a variable while preserving its exit
code, then explicitly check whether the command succeeded before deciding to
skip the object. Only increment SKIPPED and return 0 if kubectl succeeded and
returned empty output (meaning the object was genuinely deleted), otherwise
propagate the error so the migration stops and alerts the operator to the
API/RBAC problem.
- Line 1: Change the shebang from `#!/bin/bash` to `#!/bin/sh` and rewrite the
migration script using POSIX-compatible syntax. Specifically, replace the
Bash-only syntax constructs: replace the tab character escape `$'\t'` at line 85
with a literal tab character (or printf equivalent), and replace the here-string
operator `<<<` at line 88 with a here-document or pipe approach. Additionally,
modify the error handling around lines 55-56 where `kubectl get` errors are
silently suppressed with `2>/dev/null` to distinguish between a missing CRD
(which is safe to skip) and actual API call failures (which should trigger a
retry or explicit error). Finally, at line 65, replace the merge patch operation
on the entire `.spec.nodeGroups` object with a strategic merge patch or
individual field patches to avoid overwriting concurrent edits to other node
groups made by users or controllers.
🪄 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: 2d23c207-802a-4ced-a104-662b65f1be75

📥 Commits

Reviewing files that changed from the base of the PR and between 39f3ac7 and d73d1d9.

📒 Files selected for processing (13)
  • api/apps/v1alpha1/kubernetes/types.go
  • hack/e2e-apps/run-kubernetes.sh
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/templates/_helpers.tpl
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/templates/dashboard-resourcemap.yaml
  • packages/apps/kubernetes/tests/values-ci.yaml
  • packages/apps/kubernetes/tests/values/common.yaml
  • packages/apps/kubernetes/values.schema.json
  • packages/apps/kubernetes/values.yaml
  • packages/core/platform/images/migrations/migrations/44
  • packages/core/platform/values.yaml
  • packages/system/kubernetes-rd/cozyrds/kubernetes.yaml

Comment thread packages/core/platform/images/migrations/migrations/44 Outdated
Comment thread packages/core/platform/images/migrations/migrations/44 Outdated
Comment thread packages/core/platform/images/migrations/migrations/44 Outdated
@myasnikovdaniil
myasnikovdaniil force-pushed the fix/kubernetes-md0-removable branch from d73d1d9 to fb0d301 Compare June 22, 2026 05:14
@myasnikovdaniil myasnikovdaniil self-assigned this Jun 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NOT LGTM (REQUEST_CHANGES)

Business context

The kubernetes app baked a default md0 node group into values.yaml, which Helm merged into every cluster and could not be removed; this PR moves the default into a kubernetes.nodeGroups helper applied only when nodeGroups is empty, and adds migration 44 to pin md0 explicitly on existing clusters so the chart change does not prune their live MachineDeployment.

Migration activation is correct: migrations.targetVersion is bumped 44 → 45, which activates /migrations/44 (not dead code). Evidence: packages/core/platform/values.yaml:9. The helper change is also complete — every template that iterated nodeGroups now routes through the helper, so no template renders an empty worker set. Evidence: packages/apps/kubernetes/templates/cluster.yaml:240, packages/apps/kubernetes/templates/dashboard-resourcemap.yaml:33.

Blockers

B1 — Version stamp drops the platform.cozystack.io/no-delete label

  • File: packages/core/platform/images/migrations/migrations/44:102-104
  • Evidence: the stamp is kubectl create configmap --namespace cozy-system cozystack-version --from-literal version=45 --dry-run=client --output yaml | kubectl apply --filename - — a label-less manifest. Migrations 42 and 43 both stamp via an inline manifest carrying labels: { platform.cozystack.io/no-delete: "true" }, and migrations/43 documents exactly this case: "A label-less apply by the same field manager ... would strip the platform.cozystack.io/no-delete label."
  • Impact: applied by the same field manager, the label-less manifest strips no-delete from the cozystack-version ConfigMap, removing it from the deletion guardrail. Upgrade-path only, so E2E does not exercise it.
  • Fix: stamp version 45 with the labeled inline manifest used by migrations/43.

B2 — Fail-open on patch/read failure lets the destructive chart change proceed

  • File: packages/core/platform/images/migrations/migrations/44:55-56,97-100
  • Evidence: the per-object read uses kubectl get ... 2>/dev/null || true, turning any non-NotFound error into a SKIP (lines 55-56); and on FAILURES > 0 the script logs a WARN and exit 0 (lines 97-100), so the pre-upgrade hook succeeds even when some clusters were not pinned.
  • Impact: with the hook green, the platform upgrade completes and the new kubernetes chart rolls out to tenant clusters. An unpinned cluster (CR still without md0) then has its live md0 MachineDeployment pruned (→ CAPI Machine/node deletion) — the exact outcome this migration exists to prevent. Leaving the stamp at 44 for retry does not help, because the chart has already rolled out before the next upgrade.
  • Fix: treat non-NotFound read errors as failures rather than skips, and make the failure path block the chart change (fail the hook) instead of completing the upgrade with clusters left unpinned.

Non-blocking

  1. No test covers the empty-nodeGroupsmd0 default. Evidence: packages/apps/kubernetes/tests/values-ci.yaml changed from nodeGroups: null to an explicit md0, and no tests/*_test.yaml asserts that an empty nodeGroups renders the default md0 MachineDeployment. The new helper's core behavior is therefore unverified by helm-unittest.

@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) addressed in c97cfb4 / 1252378:

  • B1 (no-delete label) — the version stamp now uses the labeled here-doc (mirroring migration 43), so the platform.cozystack.io/no-delete label survives; the label-less kubectl create configmap | apply is gone.
  • B2 (fail-open) — the per-object read now distinguishes a genuine NotFound (skip) from any other error (RBAC/transient → counted as a failure), and the script exit 1s when FAILURES > 0, so the pre-upgrade hook fails and Helm aborts the upgrade before the new chart can prune the live md0 MachineDeployment on an un-pinned cluster. Header comment updated to the fail-closed contract.
  • N1 (test) — added nodegroups_default_test.yaml: with an empty nodeGroups, the kubernetes.nodeGroups helper renders the default md0 MachineDeployment with the default 20Gi disk.
  • CodeRabbit (patch scope) — the merge patch is narrowed to {spec:{nodeGroups:{md0:…}}} so a concurrent edit to a sibling group can't be reverted.
  • CodeRabbit (POSIX #!/bin/sh) — declining: the migrations image installs bash and the script uses bash-only features (local, <<< here-strings), so #!/bin/bash is honored. The gemini --filename/--patch-file nits are mooted by the here-doc rewrite / unaffected.

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.

NOT LGTM — both prior blockers are genuinely fixed and well-tested; one gap remains: the fail-closed safety branch (which is what prevents data loss) has no automated test, and it is testable with a pattern this repo already uses.

Credit where due — the fixes are solid and verified:

  • B1 (version stamp dropped the no-delete label): fixed. Migration 45 stamps via the shared stamp_cozystack_version helper (:131), which emits the ConfigMap carrying platform.cozystack.io/no-delete, and the cozystack-version-stamp.bats suite structurally enforces that every migration stamps through that helper, so it can't regress to a label-less apply.
  • B2 (fail-open let the destructive chart change proceed): fixed. The migration is now fail-closed — set -euo pipefail (:23), per-object failures counted instead of skipped (:73, :103), list errors abort (:108), and FAILURES > 0 exits 1 before the version stamp (:121-123), so the upgrade halts and Helm never rolls the new chart against clusters whose md0 isn't pinned.
  • The empty-nodeGroups -> md0 default is now pinned by tests/nodegroups_default_test.yaml, run under make unit-tests.

Blocker

B1: the fail-closed abort path has no regression test

The FAILURES > 0 -> exit 1 before the stamp branch is the entire safety mechanism of this migration — if it ever regresses to fail-open, the destructive chart change proceeds against un-pinned clusters and their live md0 MachineDeployment is pruned (the exact data loss this PR exists to prevent). That branch is currently verified by reading only. It is testable with the kubectl-mock bats pattern this repo already uses in cozystack-version-stamp.bats.

Fix: add a bats case that mocks a kubectl get/patch failure and asserts the migration exits non-zero before the version stamp is written (i.e. CURRENT_VERSION is not advanced). A negative helm-unittest (a custom nodeGroups without md0 renders no *-md0 MachineDeployment) would also be worth adding to pin the removability branch directly, but the fail-closed test is the one that guards against data loss.

@myasnikovdaniil
myasnikovdaniil force-pushed the fix/kubernetes-md0-removable branch from 933ab5d to f3bfe6c Compare June 29, 2026 09:24
@github-actions github-actions Bot added size/XL This PR changes 500-999 lines, ignoring generated files and removed size/L This PR changes 100-499 lines, ignoring generated files labels Jun 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
hack/kubernetes-md0-migration.bats (1)

85-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a dry-run regression case too.

This suite never exercises MIGRATION_DRY_RUN=1, so the success-on-read-failure path in the migration script slips through. A single MOCK_FAIL=read dry-run test would lock that branch down once the exit ordering is fixed.

Possible addition
+@test "migration 47 dry-run still exits non-zero when a read fails" {
+  tmp=$(mktemp -d)
+  export KLOG="$tmp/kubectl.log"
+  : > "$KLOG"
+  write_fake_kubectl "$tmp" "$KLOG"
+
+  if MIGRATION_DRY_RUN=1 KLOG="$KLOG" MOCK_FAIL=read PATH="$tmp:$PATH" "$MIGRATION" >"$tmp/out" 2>&1; then
+    echo "expected dry-run migration to exit non-zero on a non-NotFound read failure; output:" >&2
+    cat "$tmp/out" >&2
+    rm -rf "$tmp"
+    exit 1
+  fi
+
+  if grep -q apply "$KLOG"; then
+    echo "dry-run must not stamp the version" >&2
+    cat "$KLOG" >&2
+    rm -rf "$tmp"
+    exit 1
+  fi
+  rm -rf "$tmp"
+}
🤖 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 `@hack/kubernetes-md0-migration.bats` around lines 85 - 154, Add a regression
test for the dry-run path in the kubernetes migration suite, since
`MIGRATION_DRY_RUN=1` is currently untested. Extend the `migration 47` cases in
`hack/kubernetes-md0-migration.bats` with a `MOCK_FAIL=read` dry-run scenario
using `write_fake_kubectl` and the `MIGRATION` runner, and assert the script
still exits appropriately without advancing the version stamp. Make sure the new
test specifically covers the success-on-read-failure branch so the dry-run
behavior stays locked down.
🤖 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 `@hack/e2e-apps/run-kubernetes.sh`:
- Around line 154-170: The new kubectl wait calls in run-kubernetes.sh can fail
early with NotFound if the target resources are not created yet, so add the same
existence backstop pattern used earlier in the script before each wait. Update
the sections around kubernetes-${test_name},
kubernetes-${test_name}-cluster-autoscaler/kccm/kcsi-controller, and
kubernetes-${test_name}-md0 to first wait for object existence, then run the
existing condition waits. Keep the fix aligned with the existing event-driven
check style already present in the script.
- Around line 730-761: The server-side dry-run fallback in run-kubernetes.sh
writes stderr to a predictable /tmp path, which can be tampered with by other
processes. Replace the hardcoded /tmp/sc-fallback-render.err usage in the helm
install block with a unique tempfile created via mktemp, store it in a variable,
and use that variable consistently for both the redirected stderr and the later
cat/readback. Make sure the cleanup path removes the tempfile after use and keep
the existing rc/raw handling intact.

In `@packages/core/platform/images/migrations/migrations/47`:
- Around line 116-123: The dry-run path in the migration script is returning
success before checking whether any reads or patches failed, so non-NotFound
errors can be hidden. Update the logic around the DRY_RUN and FAILURES checks in
the migration script for version 47 so dry-run still reports failures from
patch_md0() and only exits 0 when FAILURES is zero; keep the existing
version-stamp gating behavior intact.

---

Nitpick comments:
In `@hack/kubernetes-md0-migration.bats`:
- Around line 85-154: Add a regression test for the dry-run path in the
kubernetes migration suite, since `MIGRATION_DRY_RUN=1` is currently untested.
Extend the `migration 47` cases in `hack/kubernetes-md0-migration.bats` with a
`MOCK_FAIL=read` dry-run scenario using `write_fake_kubectl` and the `MIGRATION`
runner, and assert the script still exits appropriately without advancing the
version stamp. Make sure the new test specifically covers the
success-on-read-failure branch so the dry-run behavior stays locked down.
🪄 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: 51afc051-e43c-48c9-9788-9331a7c3c922

📥 Commits

Reviewing files that changed from the base of the PR and between 933ab5d and f3bfe6c.

📒 Files selected for processing (14)
  • api/apps/v1alpha1/kubernetes/types.go
  • hack/e2e-apps/run-kubernetes.sh
  • hack/kubernetes-md0-migration.bats
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/templates/_helpers.tpl
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/templates/dashboard-resourcemap.yaml
  • packages/apps/kubernetes/tests/nodegroups_default_test.yaml
  • packages/apps/kubernetes/tests/values-ci.yaml
  • packages/apps/kubernetes/tests/values/common.yaml
  • packages/apps/kubernetes/values.schema.json
  • packages/apps/kubernetes/values.yaml
  • packages/core/platform/images/migrations/migrations/47
  • packages/core/platform/values.yaml
✅ Files skipped from review due to trivial changes (2)
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/tests/values-ci.yaml
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/apps/kubernetes/templates/dashboard-resourcemap.yaml
  • packages/apps/kubernetes/values.yaml
  • packages/apps/kubernetes/tests/values/common.yaml
  • packages/apps/kubernetes/values.schema.json
  • packages/apps/kubernetes/templates/_helpers.tpl
  • api/apps/v1alpha1/kubernetes/types.go

… empty

The chart shipped a default nodeGroups.md0 in values.yaml. Helm deep-merges
values, so the default md0 was re-added on top of any user-supplied
nodeGroups; combined with Kubernetes stripping null values, md0 could never
be removed by configuration. Move the default into the template
("kubernetes.nodeGroups" helper) so it is applied only when nodeGroups is
empty. User-supplied node groups now fully replace the default, letting users
name and omit groups (including md0) freely.

Test fixtures that relied on the chart-default md0 to supply diskSize via
values merge now set it explicitly.

Signed-off-by: mattia-eleuteri <mattia@hidora.io>
mattia-eleuteri and others added 5 commits June 30, 2026 12:08
After the kubernetes chart stops force-merging a default md0 node group,
clusters that relied on the implicit md0 (defined other nodeGroups without
md0) would have their live md0 MachineDeployment pruned by Helm on the next
reconcile. Migration 47 makes md0 explicit on those clusters to preserve the
current node topology; it is idempotent, treats a missing CRD as a no-op, and
skips clusters that already define md0.

Signed-off-by: mattia-eleuteri <mattia@hidora.io>
…e label

Migration 47 pins the implicit default md0 node group before the kubernetes
chart upgrade so the live md0 MachineDeployment is not pruned. Two correctness
fixes:

- Stamp the cozystack-version ConfigMap via the shared stamp_cozystack_version
  helper instead of `kubectl create configmap | kubectl apply`. The label-less
  apply by the same field manager stripped the platform.cozystack.io/no-delete
  label migration 42 added, dropping cozystack-version out of the
  cozystack-no-delete-guardrail policy; the helper re-emits the label and is
  enforced for every migration >= 42 by hack/cozystack-version-stamp.bats.
- Fail closed. A non-NotFound read error is now counted as a failure rather
  than silently skipped, and any failure exits 1 so the pre-upgrade hook aborts
  before Helm rolls out the new chart against clusters whose md0 is not yet
  pinned. The version stamp stays at 47 and the migration retries next upgrade.

Also narrow the merge patch to only md0 so a concurrent edit to a sibling node
group between the read and the patch cannot be reverted.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Assert that an empty nodeGroups renders the built-in md0 group via the
kubernetes.nodeGroups helper else-branch (the behaviour that makes md0
removable): the default md0 MachineDeployment is emitted and the default 20Gi
system disk flows through. A minimal VirtualMachineClusterInstancetype plus
dummy CAPI objects are registered so the per-group instanceType lookup and the
old-template preservation LISTs resolve under helm-unittest.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The existing case pins the empty-nodeGroups -> default md0 path (the helper's
else-branch). This adds the complementary if-branch: when a user supplies their
own nodeGroups, they are authoritative and the built-in md0 is NOT merged in, so
md0 is removable. The assertion selects every rendered MachineDeployment and
requires it to be the user's own group, so a regressed md0 leak surfaces as a
second MachineDeployment named <release>-md0 that fails the assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Migration 47's safety property is that any per-object read or patch failure
aborts the migration (exit 1) before the cozystack-version stamp advances, so
the pre-upgrade hook fails and Helm never rolls the new kubernetes chart against
an un-pinned cluster (whose live md0 MachineDeployment would otherwise be
pruned). That branch was verified by reading only.

Add hack/kubernetes-md0-migration.bats, mirroring the kubectl-mock style of the
plain-shell cozytest.sh runner: a fake kubectl on PATH simulates a non-NotFound
read failure and a patch failure, and the tests assert the migration exits
non-zero and never reaches the version stamp (no `kubectl apply` of
cozystack-version is logged). A positive-control case asserts a clean run does
reach the stamp, so the "no stamp" assertions are meaningful rather than vacuous.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
@myasnikovdaniil
myasnikovdaniil force-pushed the fix/kubernetes-md0-removable branch from d5b2e33 to 4c5a8c8 Compare June 30, 2026 07:16

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 remaining blocker is resolved and the migration's correctness re-verified after the rebase to /47.

The fail-closed abort path now has a regression test: hack/kubernetes-md0-migration.bats puts a fake kubectl on PATH and asserts that a read failure and a patch failure each abort non-zero WITHOUT the version stamp ever running, with a clean-run positive control so the "no stamp" assertions aren't vacuous. It is a unit bats (not e2e-prefixed), so BATS_UNIT_FILES picks it up under make unit-tests, and it respects the cozytest.sh constraints (no run/$status, no column-0 }, inline cleanup).

Re-verified after the renumber to /47:

  • Activation is correct: targetVersion 47→48 activates migration /47, and main is at 46/47 so there is no /47 collision.
  • Fail-closed survived the rebase: set -euo pipefail, per-object failures counted rather than skipped, list errors abort, and FAILURES>0 -> exit 1 before the stamp; the stamp goes through stamp_cozystack_version 48, so the cozystack-version ConfigMap keeps the platform.cozystack.io/no-delete label.
  • patch_md0 is data-safe: it distinguishes NotFound (skip) from other read errors (failure), and patches only {spec:{nodeGroups:{md0}}}, so a concurrent edit to sibling node groups is not clobbered.
  • No live drift: the injected MD0_JSON is byte-identical to both the new kubernetes.nodeGroups helper default and the previous baked values.yaml md0, so pinning an existing cluster leaves its md0 MachineDeployment unchanged.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 50748fe into main Jun 30, 2026
40 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the fix/kubernetes-md0-removable branch June 30, 2026 09:08
@github-actions

Copy link
Copy Markdown

Backport failed for release-1.5, because it was unable to cherry-pick the commit(s).

Please cherry-pick the changes locally and resolve any conflicts.

git fetch origin release-1.5
git worktree add -d .worktree/backport-2936-to-release-1.5 origin/release-1.5
cd .worktree/backport-2936-to-release-1.5
git switch --create backport-2936-to-release-1.5
git cherry-pick -x 7e3c5f99dbed00cc0459cdf4a61869bfe844f434 602b8d06fdf3a8f0844276e9dd686a63ad3a27d8 b9749cec803bb25b31c29ee9e17aeb5f312c84c0 ab55ff192407079ef33eccffcf9583ab4d09f986 c818ece25b20eb7e4ce7315b079d9627362557c8 4c5a8c830c41ec58e3ff62659d1b1fbfe29fb07a

@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Not backporting this to release-1.5, recording the reason here so the next patch cut does not re-open the question.

This carries an api/apps/v1alpha1 type change, a values.schema.json change and a new migration. On a patch line the migration is the blocking part: slots 46 and 47 on release-1.6 hold the v1.30 to v1.31 tenant version bump and the md0 node-group pin, so claiming a slot here and raising targetVersion would make a cluster that takes the patch and then upgrades to 1.6 skip them — the version bump in particular is backed by a hard fail() guard in the 1.6 chart's templates/_versions.tpl, so skipping it blocks the upgrade outright. That is the forward-upgrade trap in docs/release.md under "Backporting code that touches monotonic counters", and shipping the migration without the counter bump would leave it inert.

The change also cherry-picks with conflicts in four files, including packages/apps/kubernetes/templates/cluster.yaml and packages/core/platform/values.yaml, so it is not a mechanical carry.

Removing the backport label so cmd/backport-audit stops reporting it as outstanding for this line. Note the tool can only represent a deliberate drop when a backport PR exists to close — there was never one here, which is why the record lives in this comment.

@myasnikovdaniil myasnikovdaniil removed the backport Should change be backported on previous release label Aug 4, 2026
myasnikovdaniil added a commit that referenced this pull request Aug 4, 2026
…roup (#3535)

## What this PR does

Fixes #3504.

The chart has two loops over worker node groups and they read different
sources. `templates/cluster.yaml` iterates the effective set through the
`kubernetes.nodeGroups` helper, whose else-branch supplies the built-in
`md0` group when the user declares none — that is how #2936 made `md0`
removable without a Helm values merge re-adding it.
`templates/talos/talos-reconcile-job.yaml` iterated the raw
`.Values.nodeGroups` map instead, which is empty in exactly that case.

The consequence is a dangling reference. The chart cannot render the
`TalosConfigTemplate` itself — it needs the apiserver Service ClusterIP,
the Talos CA and the Kubernetes CA, none of which exist while Helm is
executing the template — so `cluster.yaml` deliberately stopped
rendering it and the Job became its only producer, while the
`MachineDeployment` still names it in
`spec.template.spec.bootstrap.configRef`. On a cluster with the default
`nodeGroups`, `MachineDeployment/<release>-md0` renders and zero Jobs
render, so `TalosConfigTemplate/<release>-md0` is never created and CAPI
blocks every Machine that ever joins the group. The same Job also
patches `KamajiControlPlane.spec.network.certSANs` with the live Service
ClusterIP, so that is skipped too.

Nothing fails at install time, because the built-in group carries
`minReplicas: 0`. The failure waits for the first scale-up, which is the
documented path rather than a corner: `values.yaml` tells operators that
enabling the ingress-nginx addon on a cluster with default `nodeGroups`
means waiting for the cluster-autoscaler to bring `md0` up in response
to the controller Pods becoming Pending, and the autoscaler is deployed
unconditionally whenever the tenant has an etcd DataStore.

The fix is to range over the helper so the Job set tracks the
MachineDeployment set exactly. A cluster that declares its own groups
renders byte-identically — which is why the pinned content-hash fixtures
in `tests/talos_templates_test.yaml` are untouched — and `md0` stays
removable, because the fix defers to the helper rather than merging
`md0` in unconditionally. On upgrade it is additive: the Job appears
where there was none, applies the `TalosConfigTemplate` the
MachineDeployment already expects, and the content-hash name suffix
means Helm creates a fresh Job rather than attempting to patch an
immutable one.

### How this survived

Every helm-unittest fixture and every e2e suite declares `md0`
explicitly. `tests/nodegroups_default_test.yaml` does render the
empty-`nodeGroups` case, but lists only `templates/cluster.yaml`, so it
never looked at a Job. The two OIDC chainsaw lanes do install with an
empty map, but assert that the HelmRelease exists rather than that it
becomes Ready, so a worker that never boots is invisible to them.
Offline `helm template` cannot reach this path at all: `cluster.yaml`'s
instanceType validator needs a live `lookup` for `u1.medium`.

`tests/talos_reconcile_nodegroups_test.yaml` closes the gap and pins
both halves of the helper contract — an empty map yields exactly one
`md0` Job carrying `GROUP_NAME=md0`, and a two-group map yields exactly
two Jobs with no `md0` among them, so a future fix that merges `md0` in
unconditionally fails here. Reverting the template change fails the
first case on the document count, so it guards the behaviour rather than
restating it.

### Screenshots

Not a UI change.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff: it is one Helm template loop and one new helm-unittest suite
inside `packages/apps/kubernetes`. No package is added, renamed or
removed; no `values.yaml`, `values.schema.json`,
`ApplicationDefinition`, version enum or default changes; nothing under
`hack/`, no namespace, variant, label, annotation or metric renamed. The
behaviour now matches what `values.yaml` already documents, so no
reference page drifts.

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

```release-note
fix(kubernetes): a tenant Kubernetes cluster created without an explicit `nodeGroups` map now gets a talos-reconcile Job for the built-in `md0` node group. Previously that Job rendered only for explicitly declared groups, so the default `md0` MachineDeployment referenced a TalosConfigTemplate nothing ever created: every worker the cluster-autoscaler added to `md0` stayed stuck waiting for its bootstrap config, and the KamajiControlPlane certSANs patch the same Job performs never ran. Clusters that declare their own node groups were unaffected.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Ensured the built-in `md0` node group receives its Talos
reconciliation Job when no node groups are configured.
* Prevented duplicate reconciliation Jobs when custom node groups are
specified.
* Preserved correct group-specific settings and control-plane
configuration in generated Jobs.

* **Tests**
* Added regression coverage for default and custom node-group scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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 area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) kind/bug Categorizes issue or PR as related to a bug size/XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants