Skip to content

fix(platform): migrate ephemeralStorage to diskSize via pre-upgrade hook - #2688

Merged
myasnikovdaniil merged 1 commit into
mainfrom
fix/kubernetes-ephemeral-storage-migration
May 21, 2026
Merged

fix(platform): migrate ephemeralStorage to diskSize via pre-upgrade hook#2688
myasnikovdaniil merged 1 commit into
mainfrom
fix/kubernetes-ephemeral-storage-migration

Conversation

@IvanHunters

@IvanHunters IvanHunters commented May 19, 2026

Copy link
Copy Markdown
Collaborator

Problem

PR #2454 renamed nodeGroups[*].ephemeralStoragenodeGroups[*].diskSize with
a hard {{ fail }} guard. Any cluster whose HelmRelease still carries the legacy
field cannot be reconciled by Flux at all — unrelated control-plane changes and
MachineHealthCheck remediations are also blocked.

Solution

Add platform migration 41 that runs as a pre-upgrade hook before any chart
resources are applied. The migration walks every kuberneteses.apps.cozystack.io
Application CR cluster-wide and renames nodeGroups[*].ephemeralStorage to
nodeGroups[*].diskSize, preserving the user's value.

  • Idempotent: a second run is a no-op (field is already absent)
  • Best-effort: a failed patch is logged; the version stamp stays at 41 so the
    migration retries on the next platform upgrade
  • Bumps migrations.targetVersion 41 → 42

The chart-side guard remains in place with an updated error message that directs
operators to the migration job logs if the field somehow reappears post-upgrade.

Testing

Verified on dev cluster: migration correctly renames ephemeralStorage values
in existing Application CRs. All 123 helm unit tests pass.

Summary by CodeRabbit

Release Notes

  • Documentation

    • Platform migration 41 now automatically handles the ephemeralStorage to diskSize field rename for Kubernetes node groups
    • Migration is transparent during upgrade with no manual configuration changes required
    • Error messages updated with clearer troubleshooting guidance
  • Tests

    • Updated test assertions for migration error messages

Review Change Stack

@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 the upgrade experience for Cozystack by relaxing constraints on deprecated configuration fields. By removing the strict validation that caused template rendering to fail when 'ephemeralStorage' was present, the operator can now successfully reconcile existing Kubernetes application objects while transitioning to the newer 'diskSize' field.

Highlights

  • Removed deprecation guard: Removed the hard failure check for the deprecated 'ephemeralStorage' field in the Kubernetes cluster template to prevent reconciliation blocks during upgrades.
  • Updated test suite: Updated unit tests to verify that configurations containing 'ephemeralStorage' now reconcile successfully and correctly prioritize 'diskSize' where applicable.
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.

@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 May 19, 2026
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds platform migration 41 to automatically rename nodeGroups[*].ephemeralStoragenodeGroups[*].diskSize for existing Kubernetes custom resources. The migration script patches all affected objects idempotently, bumps the platform migration target version from 40 to 42, and updates template/test/docs messaging to reference the migration and migration-hook logs.

Changes

ephemeralStorage → diskSize migration

Layer / File(s) Summary
Migration script (41) implementation
packages/core/platform/images/migrations/migrations/41
New migration script that lists all kuberneteses.apps.cozystack.io objects, applies a jq transformation to copy ephemeralStorage into diskSize (when diskSize absent) and null out ephemeralStorage for merge-patch deletion, supports dry-run and idempotent retry, tracks outcomes, and advances cozystack-version ConfigMap from 41 to 42 on success.
Platform migration version bump
packages/core/platform/values.yaml
migrations.targetVersion updated from 40 to 42 to declare the new migration as active.
Template, test, and README messaging
packages/apps/kubernetes/templates/cluster.yaml, packages/apps/kubernetes/tests/cluster_test.yaml, packages/apps/kubernetes/README.md
Cluster template fail message and test assertions updated to reference migration 41 and direct users to check cozystack-migration-hook Job logs; README Breaking Changes note rewritten to describe the rename as versioned (v1.4) with automatic transparent migration during pre-upgrade hook.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A field renamed with gentle care,
Through migration scripts that roam each sphere,
From ephemeralStorage old and worn,
To diskSize fresh, new-minted, born—
No breaking changes here to fear,
Just magic hooks making all clear! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing a migration that renames ephemeralStorage to diskSize via a pre-upgrade hook, which is the primary objective across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/kubernetes-ephemeral-storage-migration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot dosubot Bot added the backport Should change be backported on previous release label May 19, 2026

@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 removes the migration guard for the deprecated ephemeralStorage field and updates tests to allow its presence without failing. However, the current implementation silently ignores the field, defaulting to 20Gi instead. Feedback indicates this is a regression and suggests using ephemeralStorage as a fallback for diskSize in the template and updating the tests to verify this behavior.

{{- if hasKey .group "ephemeralStorage" }}
{{- fail (printf "nodeGroup %q: ephemeralStorage is no longer supported. Rename it to diskSize. See README.md for migration instructions." .groupName) }}
{{- end }}
source:

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.

high

While removing the fail guard prevents upgrade blocks, the current implementation silently ignores ephemeralStorage. If a user has not yet migrated their configuration to use diskSize, their nodes will be provisioned with the default 20Gi disk (as defined in values.yaml), which could lead to data loss or service failure if the previous size was larger.

To provide true backward compatibility as suggested by the PR title, consider using ephemeralStorage as a fallback for diskSize in the template (line 46). For example:

storage: {{ .group.diskSize | default .group.ephemeralStorage | default "20Gi" | quote }}

- notFailedTemplate: {}
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].spec.storage.resources.requests.storage
value: "20Gi"

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.

high

This test case asserts that the custom ephemeralStorage value (50Gi) is ignored and the default 20Gi is used instead. This confirms a regression for users who haven't migrated their configuration. If the template is updated to support ephemeralStorage as a fallback, this test should be updated to expect the provided value.

          value: 50Gi

@Arsolitt Arsolitt (Arsolitt) 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.

NOT LGTM — the user-facing migration guide still says Helm rendering will fail on ephemeralStorage, but this PR removes that behavior and instead lets the value be silently dropped in favor of the 20Gi schema default. Shipping the new behavior without updating the docs will mislead anyone following the migration guide.

Business context: After upgrade, the operator pre-fills diskSize: "20Gi" from the schema's leaf default into existing Kubernetes applications. The old chart's {{- fail }} guard then blocked reconciliation indefinitely. This PR removes the guard so upgrades unblock. The accepted tradeoff is that nodeGroups carrying only ephemeralStorage silently fall back to the schema default — users must migrate manually to preserve their disk size.

Blockers

B1: Migration guide contradicts the new behavior

File: packages/apps/kubernetes/README.md:88

The "Breaking Changes" section still states:

There is no backward-compatibility fallback; users MUST update their configurations to use diskSize instead of ephemeralStorage. If ephemeralStorage is still present in values, Helm template rendering will fail with an error directing you to use diskSize.

After this PR, both claims are wrong:

  • Rendering no longer fails — the {{- fail }} guard at templates/cluster.yaml:41-43 is removed.
  • There is a (dead-code) fallback expression in the chart, but the practical outcome is silent shrinkage to 20Gi for unmigrated configs.

A user reading the migration guide after this PR ships will:

  • See no build error during upgrade → assume the migration is complete or non-urgent.
  • Not realize their ephemeralStorage: 50Gi is silently coerced to 20Gi on next reconciliation.
  • Discover the regression only when CAPI replaces VMs with fresh 20Gi PVCs and nodes hit disk pressure or ImagePullBackOff.

Evidence: README.md:88 contradicts cluster.yaml:46. Reproduced: helm template with {nodeGroups.md0: {diskSize: "20Gi", ephemeralStorage: "100Gi"}} renders storage: "20Gi". The operator path that makes the chart's coalesce fallback unreachable: pkg/registry/apps/application/rest_defaulting.go:101-105 (fills absent property from ps.Default.Object) → values.schema.json:40 ("default": "20Gi" for diskSize).

Fix: Update README.md:88 to honestly describe the new behavior. Suggested wording (adapt as appropriate):

ephemeralStorage is deprecated and silently ignored: Setting ephemeralStorage on a nodeGroup no longer blocks upgrades, but it does NOT preserve the disk size — the field is dropped and diskSize defaults to 20Gi unless set explicitly. Any cluster that previously relied on ephemeralStorage to set a non-default size MUST migrate to diskSize before upgrading; otherwise the next reconciliation will provision smaller disk-kubelet PVCs (via CAPI rolling update) and existing nodes may hit disk pressure or ImagePullBackOff.

The PR description ("ephemeralStorage is the fallback when diskSize is absent") should be corrected in the same spirit — that statement is false because the operator's schema defaulting fills diskSize first.

Non-blocking follow-ups

  1. Simplify the chart expression. coalesce .group.diskSize .group.ephemeralStorage "20Gi" at templates/cluster.yaml:46 reads as a working fallback but never reaches the ephemeralStorage branch in practice — the operator's schema defaulting (pkg/registry/apps/application/rest_defaulting.go:101-105) fills diskSize from values.schema.json:40's "default": "20Gi" before the chart sees the values. Reducing to {{ .group.diskSize | default "20Gi" | quote }} produces identical behavior without implying a fallback that doesn't exist.

  2. Make the silent-drop test self-documenting. tests/cluster_test.yaml:349-360 is named "uses diskSize default (20Gi) when ephemeralStorage is set alongside default diskSize", which reads like a benign default — but the actual behavior is silent loss of the user's 50Gi. A more explicit name and a comment would help future readers see this is documented design, not an oversight. Suggested: "ephemeralStorage is silently dropped — users with non-default sizes must migrate to diskSize to preserve them".

  3. Complete the empty-string test. tests/cluster_test.yaml:362-370 only asserts notFailedTemplate: {}. Adding an equal assertion on the final storage value would make the empty-string semantics explicit.


Note: Gemini Code Assist flagged the silent-ignore concern against the first commit before coalesce was added. This review adds new evidence — the operator-side schema defaulting in rest_defaulting.go:101-105 makes the coalesce fallback structurally unreachable, which the bot did not examine.

resources:
requests:
storage: {{ .group.diskSize | default "20Gi" | quote }}
storage: {{ coalesce .group.diskSize .group.ephemeralStorage "20Gi" | quote }}

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.

Non-blocking nit: the ephemeralStorage branch of this coalesce is unreachable — the operator's schema defaulting (pkg/registry/apps/application/rest_defaulting.go:101-105) fills diskSize from values.schema.json:40's "default": "20Gi" before the chart sees the values. Consider simplifying to {{ .group.diskSize | default "20Gi" | quote }} to avoid suggesting a fallback that never triggers. The silent-drop-to-20Gi behavior is the documented tradeoff of this PR; this comment is about clarity of the expression, not the design choice.

- notFailedTemplate: {}
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].spec.storage.resources.requests.storage
value: "20Gi"

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.

Non-blocking nit: this assertion documents a deliberate behavior — the user set ephemeralStorage: 50Gi and gets back 20Gi, i.e. the value is silently lost — but the test name reads as if it's testing a benign default. A clearer name like "ephemeralStorage is silently dropped — users must migrate to diskSize to preserve size" plus a YAML comment would make the intent obvious to future readers.

@Arsolitt Arsolitt (Arsolitt) 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 — all blockers and follow-ups from the previous review are addressed in 4f6b3c6.

  • B1 (README contradicts new behavior): README.md:88 now honestly describes the silent-drop behavior, names the operator-side cause, and includes the explicit "MUST set diskSize before upgrading" warning with the disk-pressure / ImagePullBackOff failure mode.
  • Chart simplification: coalesce reverted to {{ .group.diskSize | default "20Gi" | quote }} — same behavior, no false fallback in the source.
  • Silent-drop test naming and comments: explicit and self-documenting ("ephemeralStorage is silently dropped — diskSize defaults to 20Gi regardless" + clarifying comment block).
  • Empty-string test: now asserts the final storage value, not just notFailedTemplate.

Verified locally: helm unittest tests/cluster_test.yaml — 21 passed; helm template with ephemeralStorage: 50Gi renders storage: "20Gi", matching the documented behavior.

@IvanHunters
IvanHunters force-pushed the fix/kubernetes-ephemeral-storage-migration branch from 4f6b3c6 to 016af88 Compare May 20, 2026 12:20
@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 May 20, 2026
@IvanHunters IvanHunters changed the title fix(kubernetes): treat ephemeralStorage as deprecated, do not fail on upgrade fix(platform): migrate ephemeralStorage to diskSize via pre-upgrade hook May 20, 2026
@github-actions github-actions Bot added the area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) label May 20, 2026

@Arsolitt Arsolitt (Arsolitt) 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 — solid migration design that follows the established #39 pattern. Three coordination concerns worth surfacing before merge; none of them are correctness bugs in the code itself.

Business context: PR #2454 (v1.4.0) renamed nodeGroups[*].ephemeralStoragenodeGroups[*].diskSize with a hard {{ fail }} guard. On clusters with existing tenant Kubernetes apps still carrying the legacy field, the chart fails to render at all — so the tenant kubernetes HelmRelease cannot reconcile, blocking unrelated control-plane updates and MachineHealthCheck remediations. This PR adds platform migration 40 as a pre-upgrade hook that walks every kuberneteses.apps.cozystack.io CR cluster-wide and renames the field in-place, while keeping the chart-side guard as a safety net (with an updated message that points operators at the migration job logs).

Non-blocking follow-ups

  1. Migration slot 40 is contested with PR #2650. PR #2650 (feat(platform): add deletion-protection guardrail via ValidatingAdmissionPolicy) is also OPEN and adds a different migration at the exact same path: commit cc107fe54 writes packages/core/platform/images/migrations/migrations/40 (backfills platform.cozystack.io/no-delete=true on the cozystack-version ConfigMap) and bumps migrations.targetVersion 40 → 41 in the same line of values.yaml. Hard conflict at merge time. Whichever PR lands first owns slot 40; the second must renumber to 41. For this PR specifically the renumber would rename the script, bump values.yaml:9 (targetVersion → 42), and update the literal "platform migration 40" strings baked into templates/cluster.yaml:42 and tests/cluster_test.yaml:354,364,375. The two authors should coordinate the merge order.

  2. Image rebuild is required before the migration actually runs. values.yaml:8 still pins platform-migrations:v1.4.0-rc.2@sha256:17390197... — that's the v1.4.0 image, baked before this PR added the script. The runner (run-migrations.sh:27-39) will iterate over i=40, find no /migrations/40 in that image, log "Migration 40 not found, skipping", exit 0, and the version stamp won't advance. So the rename never happens until the platform-migrations image is rebuilt with this script and values.yaml:8 is bumped. The release-tag CI handles this automatically (make image-migrations in packages/core/platform/Makefile:19-28 bumps values.yaml on tag push), and migration #39 followed exactly the same pattern (added in f18ce1974 with the v1.3.0 image still pinned, image bumped at v1.4.0-rc.2 release prep). So this is conventional — just worth confirming with maintainers that the next release tag is close enough that main-following users won't sit with the broken {{ fail }} for too long.

  3. v1.4.0 changelog misattributes the rename to migration 39. docs/changelogs/v1.4.0.md:25 and :77 claim "Migration 39 rewrites legacy ephemeralStorage values to diskSize on upgrade". Migration 39 (commit f18ce1974) is the resourcesPreset legacy-aliases → instance-type-names mapping and does NOT touch ephemeralStorage — that's the gap this PR fills. Users who upgraded to v1.4.0 expecting the automatic migration are exactly the population this PR is meant to rescue. Either v1.4.0 docs should be corrected in a separate PR, or the next changelog should make the actual mechanism (migration 40, this PR) explicit and call out the v1.4.0 mistake.

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

Approach is sound and a clear improvement over the previous (now-dismissed) silent-drop design — this preserves the user's storage size across what is actually more than a rename: PR #2454 also flipped ephemeral (in-VM emptyDisk) → persistent (DataVolume / PVC). The migration's .diskSize = .ephemeralStorage branch is what carries the user's intent across that semantic shift.

What I verified

Static

  • Helm unit tests pass on the kubernetes package (123/123).
  • The kuberneteses.apps.cozystack.io kind is served by the aggregated API server (cozystack-api), backed by HelmRelease objects in etcd. Patching the virtual resource flows through convertApplicationToHelmRelease (pkg/registry/apps/application/rest.go:1494-1554) — Values: app.Spec writes the spec straight onto HelmRelease.spec.values, the same field Flux uses to render the chart.
  • JQ transform behaves correctly on the standard cases (move-only, both-set, already-migrated).
  • Schema defaulting (rest_defaulting.go:101-114) only fills missing properties, so it won't overwrite the migrated diskSize and (in v1.4) won't re-introduce ephemeralStorage.
  • release-1.3 still uses ephemeralStorage natively, so no backport needed.

Dynamic (one v1.3.4 dev cluster, one synthetic Kubernetes app with nodeGroups.md0.ephemeralStorage: 33Gi, 1 worker)

  • Dry-run preview correctly identified the target CR and showed the right transform.
  • Real run: PATCHED=1, FAILURES=0. After the patch:
    • HelmRelease.spec.values.nodeGroups.md0 keys = [diskSize, instanceType, maxReplicas, minReplicas, roles]ephemeralStorage cleanly dropped (JSON merge-patch null semantics work as expected against the aggregated API server).
    • diskSize: "33Gi" — the user's value preserved.
  • Two-run idempotency: the actual stored HR data is identical across runs (diff empty). One caveat below.

Caveat worth knowing about

When the migration runs against a v1.3 cozystack-api (which still has ephemeralStorage as a schema property with default: "20Gi"), every read re-injects ephemeralStorage: "20Gi" on top of the stored HR data. The migration's idempotency check (current == new) therefore reports PATCHED=1 on the second run even though the patch is functionally a no-op (HR.spec.values unchanged).

In a real v1.3→v1.4 upgrade this is self-healing: the migration runs as the platform's pre-upgrade hook, then cozystack-api itself is upgraded to v1.4 (whose schema no longer knows ephemeralStorage), after which the idempotency check works correctly. Worth keeping in mind for anyone re-running the migration manually post-upgrade — it'll look like it patched something when it didn't.

What I did NOT verify

  • A full v1.3.4-with-broken-chart → v1.4-with-this-PR cluster upgrade cycle (helm upgrade of cozystack core, migration hook firing as a real Job, Flux picking up the new chart). The dev cluster I had access to is still on v1.3, so I tested the migration's data path in isolation. The chart-side guard's behavior is already covered by the existing helm unit tests; the only remaining gap is "does the pre-upgrade hook actually fire and run migration 40 as expected during a real platform upgrade." That's the same machinery as migrations 1–39 and isn't changed by this PR.

Non-blocking nits

  • The migration only patches via the aggregated API server. If cozystack-api is down at migration time, the migration exits non-zero and the Job retries (per backoffLimit: 3). Acceptable.
  • Like migration 39, no automated test for the script. Manual end-to-end is the only safety net. Matches existing convention.
  • Minor JQ edge case for ephemeralStorage: null — inline comment on line 33.

LGTM modulo the JQ edge case, which is non-blocking.

if type == "object" and has("ephemeralStorage") then
(if has("diskSize") and (.diskSize // "") != "" then .
else .diskSize = .ephemeralStorage end)
| .ephemeralStorage = null

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.

Minor edge case: when ephemeralStorage is set but its value is null or empty string, the else .diskSize = .ephemeralStorage branch copies the null/empty into diskSize. After JSON merge-patch:

  • diskSize: null → key dropped from the stored object → schema default 20Gi re-injected on next read. User who had ephemeralStorage: null (probably means "I cleared the field, give me default") gets 20Gi, which matches intent.
  • diskSize: "" → empty string stored → default "20Gi" in the chart template kicks in. Also matches intent.

Both outcomes are survivable, but the explicit diskSize: null patch is a little noisy in audit logs and could cause confusion if the operator stares at the diff. Consider an extra arm:

if has("diskSize") and (.diskSize // "") != "" then .
elif (.ephemeralStorage // "") != "" then .diskSize = .ephemeralStorage
else .  # neither has a meaningful value, let the schema default fill diskSize
end
| .ephemeralStorage = null

This keeps the patch minimal in the no-meaningful-value case. Non-blocking — the existing logic is functionally correct, just slightly noisier.

items=$(list_objects kuberneteses.apps.cozystack.io) || exit 1
while IFS=$'\t' read -r ns name; do
[ -z "$ns" ] && continue
patch_object kuberneteses.apps.cozystack.io "$ns" "$name" .spec

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.

Worth noting for the next reader: this only walks kuberneteses.apps.cozystack.io via the aggregated API server. Migration 39 also walks raw helmreleases.helm.toolkit.fluxcd.io because the legacy resourcesPreset field can appear in HelmReleases that aren't backed by an Application CR. For ephemeralStorage, the field only exists in the kubernetes app chart values, and the only path that creates those HelmReleases is via cozystack-api (pkg/registry/apps/application/rest.go:1494-1554 writes Values: app.Spec into the HelmRelease and the CR is the source of truth). So patching just the kuberneteses CR is sufficient here. No change requested — calling out the difference.

spec:
{{- if hasKey .group "ephemeralStorage" }}
{{- fail (printf "nodeGroup %q: ephemeralStorage is no longer supported. Rename it to diskSize. See README.md for migration instructions." .groupName) }}
{{- fail (printf "nodeGroup %q: ephemeralStorage is no longer supported and should have been automatically migrated to diskSize by platform migration 40. If you see this error after upgrading, the migration did not run — check the cozystack-migration-hook Job logs in cozy-system." .groupName) }}

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 updated error message is actionable — names migration 40 and points to the right Job log. Good.

Add pre-upgrade migration 41 that walks all kuberneteses.apps.cozystack.io
Application CRs and renames nodeGroups[*].ephemeralStorage to diskSize,
preserving the user's value.

Without this migration, clusters upgraded after PR #2454 either fail to
reconcile (hard fail blocks all Flux operations) or silently lose the user's
disk size setting (default 20Gi replaces whatever was configured).

The migration is idempotent and best-effort: a failed patch is logged and
leaves the version stamp at 41 for retry on next upgrade.

Update the chart-side guard error message to direct operators to the
migration job logs when the field appears post-upgrade (regression detector).
Bump migrations.targetVersion 41 -> 42.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
@myasnikovdaniil
myasnikovdaniil force-pushed the fix/kubernetes-ephemeral-storage-migration branch from e97e39a to ed1bb53 Compare May 21, 2026 07:15
@myasnikovdaniil
myasnikovdaniil merged commit ee9d154 into main May 21, 2026
9 checks passed
@myasnikovdaniil
myasnikovdaniil deleted the fix/kubernetes-ephemeral-storage-migration branch May 21, 2026 11:37
@github-actions

Copy link
Copy Markdown

Backport failed for release-1.4, 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.4
git worktree add -d .worktree/backport-2688-to-release-1.4 origin/release-1.4
cd .worktree/backport-2688-to-release-1.4
git switch --create backport-2688-to-release-1.4
git cherry-pick -x ed1bb53d2c33588b018b2642e3ac1fd4709fa844

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) backport Should change be backported on previous release 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