test(e2e): add chainsaw-native release upgrade testing lane - #3276
test(e2e): add chainsaw-native release upgrade testing lane#3276myasnikovdaniil wants to merge 10 commits into
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds reusable GitHub Actions for E2E sandbox lifecycle management and introduces a release upgrade E2E lane covering baseline installation, resource seeding, Cozystack upgrade, post-upgrade verification, and diagnostics. ChangesRelease upgrade E2E
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Workflow
participant SandboxActions
participant Sandbox
participant Chainsaw
participant Kubernetes
Workflow->>SandboxActions: download assets and prepare sandbox
SandboxActions->>Sandbox: run prepare-env
Workflow->>Sandbox: run upgrade-cozystack
Sandbox->>Chainsaw: execute seed and verify suites
Chainsaw->>Kubernetes: apply resources and run checks
Kubernetes-->>Chainsaw: readiness, data, and storage results
Workflow->>SandboxActions: collect reports and teardown
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
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 introduces an automated release upgrade testing lane to ensure platform stability during version transitions. By installing the previous stable release, seeding workloads with canary data, and verifying their integrity after an upgrade to the current build, the new suite provides critical validation for migration paths. The implementation uses a modular architecture with shared composite actions and reusable health checks, ensuring that the upgrade lane remains maintainable and efficient without impacting the existing E2E pipeline's critical path. 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. Ignored Files
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 introduces a release upgrade E2E testing lane to verify platform upgrades from the previous latest minor stable release to the current version. It adds composite GitHub Actions for sandbox lifecycle management, BATS orchestration scripts, and Kyverno Chainsaw suites for seeding and verifying workloads (MariaDB, PostgreSQL, Redis, VMs, and tenant Kubernetes clusters). The review feedback highlights a missing seeding file for PostgreSQL (upgrade-seed-postgres), recommends adding the --fail (-f) flag to curl when downloading assets to ensure proper error handling, suggests quoting the SANDBOX_NAME variable in shell commands to prevent word splitting, and proposes simplifying heavily escaped inline scripts in the upgrade BATS tests.
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.
| (conditions[?type == 'Ready']): | ||
| - status: "True" | ||
|
|
||
| - name: canary-intact |
There was a problem hiding this comment.
It appears that the corresponding seeding file hack/e2e-chainsaw-upgrade/seed/postgres/chainsaw-test.yaml is missing from this pull request. Without a seeding phase to initialize the upgrade_canary table and insert the test rows, the upgrade-verify-postgres verification step will fail during the upgrade test run.
| curl -sSL -H "Authorization: token ${APP_TOKEN}" -H "Accept: application/octet-stream" \ | ||
| -o _out/assets/nocloud-amd64.raw.xz \ | ||
| "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${DISK_ID}" |
There was a problem hiding this comment.
The curl command is invoked with -sSL but without -f (or --fail). If the asset download fails (e.g., due to an expired token, invalid disk ID, or network issue), curl will exit with code 0 and write the error response (often HTML or JSON) to _out/assets/nocloud-amd64.raw.xz. This causes silent failures that are harder to debug in subsequent steps. Adding -f ensures curl fails fast with a non-zero exit code on HTTP errors.
curl -fsSL -H "Authorization: token ${APP_TOKEN}" -H "Accept: application/octet-stream" \
-o _out/assets/nocloud-amd64.raw.xz \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${DISK_ID}"| run: | | ||
| cd "/tmp/$SANDBOX_NAME" | ||
| attempt=0 | ||
| until make SANDBOX_NAME=$SANDBOX_NAME prepare-env; do |
There was a problem hiding this comment.
The SANDBOX_NAME variable is unquoted when passed to make. While the sandbox name generated in this workflow is unlikely to contain spaces, it is a best practice to quote variable expansions in shell scripts to prevent word splitting and globbing issues.
until make SANDBOX_NAME="$SANDBOX_NAME" prepare-env; do| fi | ||
|
|
||
| # The stamp is set by a Job, so poll briefly rather than reading once. | ||
| timeout 120 sh -ec "until [ \"\$(kubectl get configmap cozystack-version -n cozy-system -o jsonpath='{.data.version}' 2>/dev/null)\" = \"${expected}\" ]; do sleep 3; done" || { |
There was a problem hiding this comment.
The inline script uses heavily escaped double quotes and variable expansions (\"\$(...) and ${expected}). This can be hard to read and maintain. You can simplify this by passing the expected version as an environment variable and using single quotes for the sh -ec script to avoid escaping. Additionally, the -o jsonpath argument does not require quotes if it contains no spaces.
EXPECTED_VERSION="$expected" timeout 120 sh -ec 'until [ "$(kubectl get configmap cozystack-version -n cozy-system -o jsonpath={.data.version} 2>/dev/null)" = "$EXPECTED_VERSION" ]; do sleep 3; done' || {
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
hack/e2e-chainsaw-upgrade/seed/tenant-k8s/chainsaw-test.yaml (1)
20-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd scoped diagnostics for the tenant-cluster resources.
Unlike the sibling seed suites (mariadb/postgres/redis/vm), which each attach a
describe+podLogsfor their specific resource incatch:, this test only hasevents: {}. Given this is explicitly called out as the flakiest/heaviest suite (nested KVM + DRBD + tenant Talos image import), a failure here would benefit most from adescribeof theKamajiControlPlane/TenantControlPlane/MachineDeploymenton error.♻️ Suggested catch block addition
catch: - events: {} + - describe: + apiVersion: kamaji.clastix.io/v1alpha1 + kind: KamajiControlPlane + name: kubernetes-upgrade-tk8s + - describe: + apiVersion: cluster.x-k8s.io/v1beta1 + kind: MachineDeployment + name: kubernetes-upgrade-tk8s-md0🤖 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/e2e-chainsaw-upgrade/seed/tenant-k8s/chainsaw-test.yaml` around lines 20 - 22, Update the catch block in the tenant-k8s Chainsaw test to retain the existing events diagnostics and add scoped describe diagnostics for the KamajiControlPlane, TenantControlPlane, and MachineDeployment resources, matching the diagnostic structure used by the sibling seed suites.
🤖 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 @.github/actions/e2e-prepare/action.yaml:
- Around line 28-35: Update the sandbox-name step around the shell variable key
to receive sandbox-suffix through the step’s env configuration, then reference
that environment variable in the shell condition and key extension instead of
directly interpolating inputs.sandbox-suffix. Preserve the existing default
behavior when the suffix is empty and the current SANDBOX_NAME and sandbox-name
outputs.
In `@hack/e2e-chainsaw-upgrade/verify/platform/chainsaw-test.yaml`:
- Around line 41-55: Validate that the baseline files exist before running
either comm comparison in the PersistentVolume and CrashLoopBackOff checks. Add
fail-fast guards for $dir/unbound-pv.txt and $dir/crashloop.txt, exiting nonzero
with an appropriate error when either is missing, while preserving the existing
PV failure gate and warning-only CrashLoopBackOff behavior.
---
Nitpick comments:
In `@hack/e2e-chainsaw-upgrade/seed/tenant-k8s/chainsaw-test.yaml`:
- Around line 20-22: Update the catch block in the tenant-k8s Chainsaw test to
retain the existing events diagnostics and add scoped describe diagnostics for
the KamajiControlPlane, TenantControlPlane, and MachineDeployment resources,
matching the diagnostic structure used by the sibling seed suites.
🪄 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: 6c3df8d5-fd85-4ca9-8ad1-b3245fd9eee3
📒 Files selected for processing (27)
.github/actions/e2e-collect/action.yaml.github/actions/e2e-download-assets/action.yaml.github/actions/e2e-prepare/action.yaml.github/actions/e2e-teardown/action.yaml.github/labels.yml.github/workflows/pull-requests.yamldocs/agents/e2e-testing.mdhack/e2e-chainsaw-upgrade/.chainsaw.yamlhack/e2e-chainsaw-upgrade/seed/mariadb/chainsaw-test.yamlhack/e2e-chainsaw-upgrade/seed/postgres/chainsaw-test.yamlhack/e2e-chainsaw-upgrade/seed/redis/chainsaw-test.yamlhack/e2e-chainsaw-upgrade/seed/tenant-k8s/chainsaw-test.yamlhack/e2e-chainsaw-upgrade/seed/vm/chainsaw-test.yamlhack/e2e-chainsaw-upgrade/seed/zz-baseline/chainsaw-test.yamlhack/e2e-chainsaw-upgrade/verify/mariadb/chainsaw-test.yamlhack/e2e-chainsaw-upgrade/verify/platform/chainsaw-test.yamlhack/e2e-chainsaw-upgrade/verify/postgres/chainsaw-test.yamlhack/e2e-chainsaw-upgrade/verify/redis/chainsaw-test.yamlhack/e2e-chainsaw-upgrade/verify/tenant-k8s/chainsaw-test.yamlhack/e2e-chainsaw-upgrade/verify/vm/chainsaw-test.yamlhack/e2e-install-cozystack.batshack/e2e-upgrade-apply.batshack/e2e-upgrade-install-previous.batshack/e2e-wait-hr-ready.shhack/upgrade-prev-version.shhack/upgrade-prev-version_test.batspackages/core/testing/Makefile
| dir=/workspace/_out/upgrade-baseline | ||
| echo "=== gate: no new unbound PersistentVolumes vs baseline ===" | ||
| kubectl get pv --no-headers 2>/dev/null | awk '$5 != "Bound" {print $1}' | sort > /tmp/post-unbound-pv.txt | ||
| new_pv=$(comm -13 "$dir/unbound-pv.txt" /tmp/post-unbound-pv.txt || true) | ||
| if [ -n "$new_pv" ]; then | ||
| echo "FAIL: PersistentVolumes newly unbound after upgrade:" >&2 | ||
| echo "$new_pv" >&2 | ||
| kubectl get pv 2>&1 | awk 'NR==1 || $5 != "Bound"' >&2 || true | ||
| exit 1 | ||
| fi | ||
| echo "no new unbound PVs" | ||
|
|
||
| echo "=== warning-only: new CrashLoopBackOff pods vs baseline ===" | ||
| kubectl get pods -A --no-headers 2>/dev/null | awk '/CrashLoopBackOff/ {print $1"/"$2}' | sort > /tmp/post-crashloop.txt | ||
| new_cl=$(comm -13 "$dir/crashloop.txt" /tmp/post-crashloop.txt || true) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Add baseline file existence checks before comm diffs.
The fatal PV check and warning crashloop check both use comm -13 "$dir/..." without first verifying the baseline files exist. If the seed phase didn't run (CI orchestration failure), comm fails, || true catches it, new_pv is empty, and the fatal "no new unbound PVs" gate silently passes — a false negative on the authoritative health signal. The Redis and VM verify suites both guard with [ -f "$before" ] || exit 1; this suite should be consistent.
🛡️ Proposed fix: add baseline file guards
dir=/workspace/_out/upgrade-baseline
+ [ -f "$dir/unbound-pv.txt" ] || { echo "missing baseline unbound-pv.txt (seed did not run?)" >&2; exit 1; }
+ [ -f "$dir/crashloop.txt" ] || { echo "missing baseline crashloop.txt (seed did not run?)" >&2; exit 1; }
echo "=== gate: no new unbound PersistentVolumes vs baseline ==="
kubectl get pv --no-headers 2>/dev/null | awk '$5 != "Bound" {print $1}' | sort > /tmp/post-unbound-pv.txt
new_pv=$(comm -13 "$dir/unbound-pv.txt" /tmp/post-unbound-pv.txt || true)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| dir=/workspace/_out/upgrade-baseline | |
| echo "=== gate: no new unbound PersistentVolumes vs baseline ===" | |
| kubectl get pv --no-headers 2>/dev/null | awk '$5 != "Bound" {print $1}' | sort > /tmp/post-unbound-pv.txt | |
| new_pv=$(comm -13 "$dir/unbound-pv.txt" /tmp/post-unbound-pv.txt || true) | |
| if [ -n "$new_pv" ]; then | |
| echo "FAIL: PersistentVolumes newly unbound after upgrade:" >&2 | |
| echo "$new_pv" >&2 | |
| kubectl get pv 2>&1 | awk 'NR==1 || $5 != "Bound"' >&2 || true | |
| exit 1 | |
| fi | |
| echo "no new unbound PVs" | |
| echo "=== warning-only: new CrashLoopBackOff pods vs baseline ===" | |
| kubectl get pods -A --no-headers 2>/dev/null | awk '/CrashLoopBackOff/ {print $1"/"$2}' | sort > /tmp/post-crashloop.txt | |
| new_cl=$(comm -13 "$dir/crashloop.txt" /tmp/post-crashloop.txt || true) | |
| dir=/workspace/_out/upgrade-baseline | |
| [ -f "$dir/unbound-pv.txt" ] || { echo "missing baseline unbound-pv.txt (seed did not run?)" >&2; exit 1; } | |
| [ -f "$dir/crashloop.txt" ] || { echo "missing baseline crashloop.txt (seed did not run?)" >&2; exit 1; } | |
| echo "=== gate: no new unbound PersistentVolumes vs baseline ===" | |
| kubectl get pv --no-headers 2>/dev/null | awk '$5 != "Bound" {print $1}' | sort > /tmp/post-unbound-pv.txt | |
| new_pv=$(comm -13 "$dir/unbound-pv.txt" /tmp/post-unbound-pv.txt || true) | |
| if [ -n "$new_pv" ]; then | |
| echo "FAIL: PersistentVolumes newly unbound after upgrade:" >&2 | |
| echo "$new_pv" >&2 | |
| kubectl get pv 2>&1 | awk 'NR==1 || $5 != "Bound"' >&2 || true | |
| exit 1 | |
| fi | |
| echo "no new unbound PVs" | |
| echo "=== warning-only: new CrashLoopBackOff pods vs baseline ===" | |
| kubectl get pods -A --no-headers 2>/dev/null | awk '/CrashLoopBackOff/ {print $1"/"$2}' | sort > /tmp/post-crashloop.txt | |
| new_cl=$(comm -13 "$dir/crashloop.txt" /tmp/post-crashloop.txt || 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 `@hack/e2e-chainsaw-upgrade/verify/platform/chainsaw-test.yaml` around lines 41
- 55, Validate that the baseline files exist before running either comm
comparison in the PersistentVolume and CrashLoopBackOff checks. Add fail-fast
guards for $dir/unbound-pv.txt and $dir/crashloop.txt, exiting nonzero with an
appropriate error when either is missing, while preserving the existing PV
failure gate and warning-only CrashLoopBackOff behavior.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/actions/e2e-prepare/action.yaml:
- Around line 3-8: Clarify the retry description in the composite action
documentation around the Talos e2e sandbox provisioning: state that the command
permits up to 3 total attempts, meaning 2 retries, and update the referenced
e2e-testing guidance citation or wording so both describe the same retry budget.
🪄 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: 2f384a97-7f00-44c4-a126-1a327aed0c6c
📒 Files selected for processing (2)
.github/actions/e2e-prepare/action.yaml.github/workflows/pull-requests.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
- .github/workflows/pull-requests.yaml
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
hack/e2e-upgrade-install-previous.bats (1)
69-73: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winAdd existence backstops before
kubectl waitand avoid ad-hoc retries.The
kubectl waiton line 69 is missing a prior existence backstop, making it vulnerable to race conditions if theDeploymentis not yet created. Furthermore, lines 72-73 use an ad-hocuntil kubectl waitretry loop instead of the established backstop pattern.As per coding guidelines for E2E scripts and learnings, every
kubectl waitmust be guarded by an existence backstop (until kubectl get), and ad-hoc retry variations should be avoided in favor of the repository's established pattern.🛠️ Proposed fix to apply the established backstop pattern
- kubectl wait deployment/cozystack-operator -n cozy-system --timeout=2m --for=condition=Available - - # Operator installs the CRDs at startup, then creates the platform PackageSource. - timeout 120 sh -ec 'until kubectl wait crd/packages.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done' - timeout 120 sh -ec 'until kubectl wait crd/packagesources.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done' + timeout 60 sh -ec 'until kubectl get deployment/cozystack-operator -n cozy-system >/dev/null 2>&1; do sleep 2; done' + kubectl wait deployment/cozystack-operator -n cozy-system --timeout=2m --for=condition=Available + + # Operator installs the CRDs at startup, then creates the platform PackageSource. + timeout 120 sh -ec 'until kubectl get crd/packages.cozystack.io crd/packagesources.cozystack.io >/dev/null 2>&1; do sleep 2; done' + kubectl wait crd/packages.cozystack.io crd/packagesources.cozystack.io --for=condition=Established --timeout=2m🤖 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/e2e-upgrade-install-previous.bats` around lines 69 - 73, Guard the deployment wait and both CRD waits with the repository’s established existence backstop: first poll using “until kubectl get” with the appropriate resource and namespace, then run “kubectl wait” once. Replace the ad-hoc “until kubectl wait” loops for packages.cozystack.io and packagesources.cozystack.io while preserving their Established condition checks and timeouts.Sources: Coding guidelines, Learnings
🤖 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.
Outside diff comments:
In `@hack/e2e-upgrade-install-previous.bats`:
- Around line 69-73: Guard the deployment wait and both CRD waits with the
repository’s established existence backstop: first poll using “until kubectl
get” with the appropriate resource and namespace, then run “kubectl wait” once.
Replace the ad-hoc “until kubectl wait” loops for packages.cozystack.io and
packagesources.cozystack.io while preserving their Established condition checks
and timeouts.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c3befbf2-52d8-4344-9c1d-0a3182ef6251
📒 Files selected for processing (1)
hack/e2e-upgrade-install-previous.bats
There was a problem hiding this comment.
🧹 Nitpick comments (1)
hack/e2e-upgrade-install-previous.bats (1)
72-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAlign with the established existence backstop pattern.
The learning explicitly advises using
until kubectl getto poll for resource existence prior to a singlekubectl waitcall, rather than wrappingkubectl waitinside the polling loop itself.As per retrieved learnings: "guard kubectl wait by first polling for the target resource to exist... Prefer the repository’s established backstop pattern rather than ad-hoc one-off variations."
♻️ Proposed refactor
- timeout 120 sh -ec 'until kubectl wait crd/packages.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done' - timeout 120 sh -ec 'until kubectl wait crd/packagesources.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done' + timeout 120 sh -ec 'until kubectl get crd/packages.cozystack.io crd/packagesources.cozystack.io >/dev/null 2>&1; do sleep 2; done' + kubectl wait crd/packages.cozystack.io crd/packagesources.cozystack.io --for=condition=Established --timeout=2m timeout 120 sh -ec 'until kubectl get packagesource cozystack.cozystack-platform >/dev/null 2>&1; do sleep 2; done'🤖 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/e2e-upgrade-install-previous.bats` around lines 72 - 74, Update the polling commands in the upgrade-install flow so each CRD first uses an until kubectl get existence loop, then performs a single kubectl wait for Established. Preserve the existing timeouts and resource names, and apply the repository’s established existence-backstop pattern consistently to packages.cozystack.io and packagesources.cozystack.io.Source: Learnings
🤖 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.
Nitpick comments:
In `@hack/e2e-upgrade-install-previous.bats`:
- Around line 72-74: Update the polling commands in the upgrade-install flow so
each CRD first uses an until kubectl get existence loop, then performs a single
kubectl wait for Established. Preserve the existing timeouts and resource names,
and apply the repository’s established existence-backstop pattern consistently
to packages.cozystack.io and packagesources.cozystack.io.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d89a1314-c216-4c79-8bc4-4fc3afe09e47
📒 Files selected for processing (4)
docs/agents/e2e-testing.mdhack/e2e-upgrade-install-previous.batspackages/core/platform/templates/migration-hook.yamlpackages/core/platform/values.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
- docs/agents/e2e-testing.md
64577bf to
6e0c374
Compare
IvanHunters
left a comment
There was a problem hiding this comment.
Verdict
LGTM with non-blocking notes
test(e2e): add chainsaw-native release upgrade testing lane (+1692/-90, 29 files). No CRITICAL/MAJOR defects after executing the available offline reproductions (helm template vs merge-base, dash/shellcheck on both new scripts, mutation-testing the new bats suite, source-level verification of the GitHub Actions / Chainsaw assumptions).
Findings
[MINOR] hack/upgrade-prev-version_test.bats:87-95 (guards hack/upgrade-prev-version.sh:41) — the test named "line match is dot-anchored (1.5 does not match v155.x)" is theatre. Removing the dot-escaping (sed 's/\./\\./g') still leaves all 10 tests green, because stable_desc's upstream filter already normalizes tag shapes so escaped vs unescaped patterns are equivalent (confirmed via a ~30k-combination differential check). A second mutation on the walk-down loop did correctly turn a different test red, so the rest of the suite is non-vacuous — only this one named test overclaims what it verifies.
Claim mismatches (non-blocking)
- [PARTIAL] PR body "Status" section is stale: commit
2c6b9140already fixes the canary DB access path the body says "still needs a dev-cluster run". - [UNVERIFIABLE] actionlint / live CI green — outside the hermetic toolset; the unit-tests portion was independently confirmed by re-running the bats suite.
Caveats (verification limits, not defects)
- Phase 5c corner-render for the new
migrations.etcdAdoptSkipBackuptoggle could not be executed:helm template packages/core/platformfails offline on a pre-existing unconditionallookup+failinrepository.yaml(reproduces identically on merge-base, not touched by this PR), andmigration-hook.yamlis itselflookup-gated. Residual risk judged low — the consumedETCD_ADOPT_SKIP_BACKUPenv var is pre-existing, already unit-tested, and the wiring is a single unconditional-else block with no cross-reference or dependsOn. - The
chart_lintrender_error + 2 missing_refs flagged by tooling are all confirmed pre-existing and unrelated (reproduced on merge-base; referencing templates untouched). - Both new
#!/bin/shscripts pass shell-portability checks (shellcheck --shell=sh, dash -n, no bashisms, invoked only via shebang). - Tenant-Kubernetes seed/verify suites are correctly gated off by default (
UPGRADE_E2E_TENANT_K8S); zero risk to default CI.
6e0c374 to
c47f709
Compare
…reate (#3319) ## What this PR does Fixes a Helm-upgrade failure in the `opensearch-operator` chart that blocks the new release-upgrade E2E lane (added in #3276). Upgrading from a chart that shipped no explicit strategy (v1.5.x) to a v1.6.0-rc build fails validation immediately, helm-controller retries it, and the HelmRelease never becomes Ready: ``` Helm upgrade failed for release cozy-opensearch-operator/opensearch-operator: Deployment.apps "opensearch-operator-controller-manager" is invalid: spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy type is Recreate ``` Observed in the ["Upgrade E2E Test" job of run 29480724982](https://github.com/cozystack/cozystack/actions/runs/29480724982) — `UpgradeFailed ... (x19 over 11m)`, with the HelmRelease never reaching Ready. ### Root cause #3040 (commit db797b1) set the single-replica operator Deployment to `strategy.type: Recreate` — a reasonable goal: with leader election gated off on one replica, a default `RollingUpdate` briefly runs two uncoordinated managers (`maxSurge`) during a rollout, and `Recreate` closes that window. The problem surfaces only on **upgrade from an older chart, under server-side apply**: - A v1.5.x release shipped no strategy at all, so the apiserver **defaulted** `spec.strategy.rollingUpdate` to `25%/25%`, and the `helm-controller` field manager never owned that block. Its `managedFields` entry covers `f:replicas`, `f:selector` and `f:template`, but not `f:strategy`. - helm-controller applies **server-side** by default (`Install.ServerSideApply` defaults to true, `Upgrade.ServerSideApply` to `auto`; `internal/operator/package_reconciler.go` sets neither), and SSA does not remove a field the applier never owned merely because the new intent omits it. - So the v1.6.0-rc manifest's `type: Recreate` merges onto a live object that still carries the defaulted `rollingUpdate`, and the apiserver rejects the result with `FieldValueForbidden`. The **client-side** path is unaffected, which is why this surfaces through helm-controller's SSA but not a local `helm upgrade`: `DeploymentSpec.Strategy` carries `patchStrategy:"retainKeys"` (`k8s.io/api@v0.34.1/apps/v1/types.go:395`), so a 3-way merge emits `{"spec":{"strategy":{"$retainKeys":["type"],"type":"Recreate"}}}`, which merges to a valid `{"strategy":{"type":"Recreate"}}` and does clear the block. This is a genuine product upgrade bug, not a test-harness issue — #3276 only adds the upgrade lane that exposes it; it does not touch `opensearch-operator`. > An earlier revision of this PR attributed the failure to Helm's client-side 3-way merge. That was wrong, and the correction is @lexfrei's — see [his review](#3319 (review)). The mechanism above is now reproduced end to end (below). ### The fix Keep `type: RollingUpdate` on the single-replica path and set `rollingUpdate.maxSurge: 0 / maxUnavailable: 1` instead of switching to `Recreate`: - `maxSurge: 0` caps total pods at `replicas`, so the deployment controller must scale the old manager down before it can scale the new one up — unlike the default 25% surge, which starts a second manager while the first is still fully live. - This is **not** `Recreate`'s exclusivity, and the PR no longer claims it is. `rolloutRecreate` gates scale-up on `oldPodsRunning()`, which counts non-terminal `Status.Phase`, so `Recreate` waits for the old pod to be gone. `rolloutRolling` has no such gate: `NewRSNewReplicas` derives `currentPodCount` from `GetReplicaCountForReplicaSets`, which sums `rs.Spec.Replicas` (desired, not live). The ordering that is guaranteed is over ReplicaSet desired counts, not pod lifecycles, so a draining old manager can still overlap the new pod within `terminationGracePeriodSeconds` (10s here). - Because `strategy.type` never becomes `Recreate`, the forbidden merge cannot occur on any apply path. It also recovers anyone already on an rc build with `Recreate`, since `Recreate -> RollingUpdate` is permitted. - This matches the idiom already vendored in this repo: `packages/system/kubeovn/charts/kube-ovn/templates/central-deploy.yaml:11-15` uses `RollingUpdate` with `maxSurge: 0 / maxUnavailable: 1`. Multi-replica installs (`manager.replicaCount > 1`) are unchanged: they keep the default `RollingUpdate` with leader election enabled. The change is carried in `patches/leaderElection.diff` so it survives a chart re-vendor via `make update`; the patch applies to pristine upstream 2.8.0 with no fuzz and reproduces the committed strategy block exactly. ### Alternatives considered | Option | Why not | | --- | --- | | Keep `Recreate`, but make the chart own `rollingUpdate` first (ship an explicit block, then switch `type` in a later release) | This is the only option that preserves real exclusivity, but it needs two releases and only works once every install has reconciled the intermediate one. Disproportionate for a single-replica operator whose residual overlap is a draining manager. | | Pre-upgrade hook / Job that patches or deletes the Deployment strategy before the upgrade | Heavy: needs a kubectl image, a ServiceAccount and RBAC to `patch`/`delete` Deployments, and hook ordering; runs on every upgrade even when unneeded; adds standing attack surface — disproportionate for a scalar field transition. | | `helm.sh/resource-policy` / force-replace | `resource-policy: keep` only governs deletion on uninstall (irrelevant). Helm has no per-resource "force recreate" annotation; `--force` / HelmRelease `spec.upgrade.force` is global and disruptive and not controllable from within the chart. | | Plain revert to default `RollingUpdate` (25%/25%) | Fixes the rejection but starts a surge pod while the old manager is still fully live — the window #3040 set out to close. `maxSurge: 0` narrows it to a draining old pod without leaving `RollingUpdate`. | ### Verification The mechanism and the fix were reproduced on kind v1.33.1 (a bare apiserver is enough — the failure is defaulting + SSA + validation): | Path | Result | | --- | --- | | SSA install of the v1.5.x manifest (no strategy) | apiserver defaults `rollingUpdate: 25%/25%`; `managedFields` shows `helm-controller` does not own `f:strategy` | | SSA upgrade to `type: Recreate` | fails with the exact production error above | | Client-side apply of the same pair | succeeds; `rollingUpdate` is cleared, live strategy becomes `{"type":"Recreate"}` | | SSA upgrade to `maxSurge: 0` (this PR) | succeeds | | SSA of `maxSurge: 0` onto a live `Recreate` Deployment (rc recovery) | succeeds | | Rollout under `maxSurge: 0` vs `Recreate` | `maxSurge: 0`: two pods coexisted within 1s (old Terminating, new ContainerCreating). `Recreate`: one pod, waited the full drain. This is the evidence for the "not exclusivity" wording above. | ### Note on regression coverage `tests/leader_election_test.yaml` was never executed: `hack/helm-unit-tests.sh` runs a package's suite only when the package Makefile defines a `test:` target, and this one did not — so the coverage the previous revision of this PR claimed did not exist. This PR adds the target (matching `packages/system/etcd-operator/Makefile`), which revives the suite; the repo-wide runner now picks the package up and a revert to `Recreate` fails it. Thanks to @lexfrei for catching that. The apiserver-side merge that produces the invalid object still needs a live cluster, so the behavioural regression test remains the release-upgrade E2E lane in #3276 (which this fix unblocks). ### Follow-ups filed - #3324 — `packages/system/ouroboros` has the same latent shape (`Recreate` gated on `controller.mode == "external-dns"`, while `controller.mode` defaults to `coredns`). Not fixed here, and it likely needs a different fix, since that mode genuinely depends on exclusivity. - #3325 — `make update` does not reproduce this package's vendored template: three hand-edits are not captured in `patches/`, and one of them breaks rendering when `manager.extraEnv` is set. Pre-existing on `main`; an earlier revision of this body claimed the regeneration was exact, which was wrong. ### Downstream repositories - [x] No downstream repository is affected by this change <!-- Walked the trigger map in docs/agents/contributing.md against the diff: the change touches only a vendored packages/system operator chart template + its patch + its helm-unittest suite + the package Makefile test target. No packages/apps or packages/extra add/rename/remove, no values.schema.json / enum / default change, no node contract, no annotations/labels, no hack/ or ApplicationDefinition change. opensearch-k8s-operator is a third-party upstream (not a cozystack downstream), and the fix lives in patches/leaderElection.diff so it survives make update. --> ### Release note ```release-note fix(opensearch-operator): fix a Helm upgrade failure on single-replica installs. Upgrading a release that predates an explicit Deployment strategy failed under server-side apply, because switching strategy.type to Recreate does not clear the apiserver-defaulted spec.strategy.rollingUpdate block that the chart never owned, and the merged object is rejected. The operator now uses RollingUpdate with maxSurge=0/maxUnavailable=1, which keeps the transition legal on every apply path and recovers releases already stuck on Recreate. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Updated OpenSearch Operator controller rollout behavior for single-replica installs to use a safer rolling update configuration. * Leader election is now applied only when multiple replicas are configured, reducing the chance of conflicting controller activity. * **Tests** * Expanded Helm unittest assertions for the controller deployment rollout strategy and leader-election behavior for single- and multi-replica scenarios. * **Chores** * Added a `make test` target to run Helm unit tests. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
c47f709 to
f6c0ed4
Compare
There was a problem hiding this comment.
NOT LGTM. The head commit rewrites two workflow gate expressions that an existing contract test pins verbatim, without updating that test. The unit-test job goes red, which skips finalize, which skips both the required E2E check and this PR's own new lane, so at this head nothing in the verification pipeline actually ran.
Business context: upgrade coverage is entirely absent today (there is no upgrade job anywhere in pull-requests.yaml on main), so a defect that only appears when an existing cluster moves to a new release ships unnoticed. This adds a lane that installs the previous stable release, seeds workloads with canary data, upgrades to the build under test, and asserts survival.
Blockers
B1: broadening the label gate breaks the contract test that guards it, which takes down the whole pipeline
File: .github/workflows/pull-requests.yaml:32 and :454
Issue: hack/promote-gate-contract.bats:122 and :126 pin these two gate expressions as exact string literals. This PR broadens both from github.event.label.name == 'full-e2e' to contains(fromJSON('["full-e2e","upgrade-e2e"]'), github.event.label.name) and does not update the test, so both grep -cF counts fall to zero.
Evidence: reproduced locally at f6c0ed4, running that contract file on its own prints Test failed: release PR E2E is a working manual full-e2e label opt-in (exit 1). The CI job log ends with the same sequence, + count=0, + [ 0 -eq 1 ], then make: *** [Makefile:165: bats-unit-tests] Error 1. In the run at this head, Unit & controller tests is failure and Finalize, E2E Tests and Upgrade E2E Test are all skipped.
Impact: the required E2E Tests check does not run on merit, it reports skipped. The new lane does not run either, even though the upgrade-e2e label is applied, so the commit titled "make the upgrade-e2e label opt-in actually start a run" currently stops every run instead. It is also why the lane's results are only visible in the run from nine days ago rather than at this head.
Fix: update both assertions to match the broadened expressions and add one asserting upgrade-e2e is in the allow-list, so the contract keeps guarding the gate rather than being routed around it.
On the lane's own red result
I checked this rather than taking it on trust, because a brand new upgrade lane failing on its first run is equally consistent with the lane being wrong. Two of the three failures carry a terminal mechanism and are platform defects, not lane defects.
The seaweedfs pair fails during template rendering: execution error at (seaweedfs/templates/seaweedfs.yaml:191:6) and (cozy-seaweedfs/templates/naming-guard.yaml:151:6), the naming guard refusing the release because volume names are derived from the release name. That is only reachable by upgrading an existing install, which is exactly the gap this lane closes. tenant-root/tenant-root then fails waiting on it, so that is a consequence rather than a fourth defect.
opensearch-operator fails with Deployment.apps "opensearch-operator-controller-manager" is invalid: spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy type is Recreate. That is the server-side-apply residue case, where the field set by the previous version survives into the new object. Upgrade-only again, and opensearch-rd is a downstream dependency of it, not a separate defect.
The third, vm-instance-test timing out on VirtualMachine/tenant-test/vm-instance-test status: 'InProgress', is a real observed regression signal, but the log does not establish a root cause, so I would not put it in the same category as the other two yet. Worth saying that explicitly rather than counting three equally proven findings.
All three belong in their own changes, so none of them block this one. I also confirmed the lane is genuinely advisory rather than taking the description's word for it: branch protection on main requires only pre-commit and E2E Tests, and this job's check name is Upgrade E2E Test, which is neither.
Non-blocking follow-ups
-
A permanently red advisory check trains reviewers to ignore it, and then it stops working on the day it catches something new. Nothing here records which failures are currently expected, so once this merges a fourth, unrelated failure is indistinguishable from the three known ones. Landing the three fixes first, or recording the known-failing set somewhere the lane prints, would keep it honest.
-
hack/e2e-chainsaw-upgrade/verify/platform/chainsaw-test.yaml:44and:55:comm -13 "$dir/unbound-pv.txt" ... || trueturns a missing baseline file into an empty diff and therefore a pass, so a verify phase run without its seed phase reports healthy having compared nothing. The sibling suite already handles this correctly atverify/vm/chainsaw-test.yaml:47, which fails fast with[ -f "$before" ] || { ...; exit 1; }. Applying the same guard here would make the two consistent. This was raised in the automated review and is still open. -
hack/upgrade-prev-version_test.bats:87: the case named "line match is dot-anchored (1.5 does not match v155.x)" does not test that. I removed thesed 's/\./\\./g'escaping athack/upgrade-prev-version.sh:41and all ten cases stayed green, becausestable_descalready constrains tags tovX.Y.Zand that makes the escaped and unescaped patterns equivalent over the resulting set. The shipped code is correct, only the test overclaims. An earlier review reached the same conclusion. The rest of the suite is not vacuous: unanchoring the stable-tag regex, and starting the minor walk atmininstead ofmin - 1, each turned a case red. -
.github/actions/e2e-download-assets/action.yaml:50:curl -sSL ... -o _out/assets/nocloud-amd64.raw.xzhas no--fail, so an auth failure or 404 gets written into the disk image while curl exits 0, surfacing much later as a confusing decompression error. The behaviour is unchanged from the code this was extracted from, but it now serves two jobs, and the release PR path is precisely the one this lane exists for. Also raised in the automated review and still open. -
The description does not name the costs it accepts: a second full platform install per run on a 32 vCPU runner with a 180 minute ceiling, doubled exposure to install-phase flakiness, and six verify suites to maintain as those apps change. I think the trade is worth making, but it should be stated rather than left implicit. The "Status" section also still says "Draft" and "unit-tests green locally", neither of which holds now.
-
hack/e2e-install-cozystack.batsis also being changed by another open PR, in the worker image catalog region rather than the readiness gate this one extracts. They should not conflict; I am flagging it only so whoever merges second knows to look.
What I checked and found clean
No EXIT or RETURN traps in either new pipeline BATS file or in any of the twelve new Chainsaw suites. hack/e2e-wait-hr-ready.sh uses if kubectl wait ...; then rather than capturing output into a variable, so it avoids the failure mode where an assignment with command substitution under set -e kills the shell before the captured output is ever printed. I checked every command-substitution assignment this diff adds and none has that shape. hack/upgrade-prev-version_test.bats is correctly picked up by the unit target rather than filtered out as an e2e file, and it declares ten cases and executes ten. The tenant Kubernetes suites gate off through an early exit 0 in a single script step and print a visible skip line. The lane runs fixed seed and verify paths with no selector, so unlike the app suite it cannot silently select nothing and report success; the one way it can report a non-failure having run nothing is the skip path in B1. The seed suites really do reuse the existing app manifests, all five relative paths resolve. Documentation is in sync, this diff adds the matching section to the E2E conventions doc.
| name: Plan build | ||
| runs-on: ubuntu-latest | ||
| if: github.event.action != 'labeled' || github.event.label.name == 'full-e2e' | ||
| if: github.event.action != 'labeled' || contains(fromJSON('["full-e2e","upgrade-e2e"]'), github.event.label.name) |
There was a problem hiding this comment.
This literal is pinned by hack/promote-gate-contract.bats:122, which is not updated in this PR, so grep -cF returns 0 and the assertion on the next line fails. That takes bats-unit-tests red, which skips finalize, which skips both E2E Tests and Upgrade E2E Test. Line 454 breaks the companion assertion at :126 the same way. Updating the contract test to the broadened expressions (and asserting upgrade-e2e is in the allow-list) keeps the guard meaningful.
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
Second round on the same head, posted separately so the additions actually reach you. Two things are new since my earlier review: a second blocker on the stale file header in the mariadb seed manifest, and a corrected statement of the version-resolver defect (the failing input is a single-component target like v1, not a two-component one). Everything else below stands as reported.
NOT LGTM. The head commit rewrites two workflow gate expressions that an existing contract test pins verbatim, without updating that test. The unit-test job goes red, which skips finalize, which skips both the required E2E check and this PR's own new lane, so at this head nothing in the verification pipeline actually ran.
Business context: upgrade coverage is entirely absent today (there is no upgrade job anywhere in pull-requests.yaml on main), so a defect that only appears when an existing cluster moves to a new release ships unnoticed. This adds a lane that installs the previous stable release, seeds workloads with canary data, upgrades to the build under test, and asserts survival.
Blockers
B1: broadening the label gate breaks the contract test that guards it, which takes down the whole pipeline
File: .github/workflows/pull-requests.yaml:32 and :454
Issue: hack/promote-gate-contract.bats:122 and :126 pin these two gate expressions as exact string literals. This PR broadens both from github.event.label.name == 'full-e2e' to contains(fromJSON('["full-e2e","upgrade-e2e"]'), github.event.label.name) and does not update the test, so both grep -cF counts fall to zero.
Evidence: reproduced locally at f6c0ed4, running that contract file on its own prints Test failed: release PR E2E is a working manual full-e2e label opt-in (exit 1). The CI job log ends with the same sequence, + count=0, + [ 0 -eq 1 ], then make: *** [Makefile:165: bats-unit-tests] Error 1. In the run at this head, Unit & controller tests is failure and Finalize, E2E Tests and Upgrade E2E Test are all skipped.
Impact: the required E2E Tests check does not run on merit, it reports skipped. The new lane does not run either, even though the upgrade-e2e label is applied, so the commit titled "make the upgrade-e2e label opt-in actually start a run" currently stops every run instead. It is also why the lane's results are only visible in the run from nine days ago rather than at this head.
Fix: update both assertions to match the broadened expressions and add one asserting upgrade-e2e is in the allow-list, so the contract keeps guarding the gate rather than being routed around it.
On the lane's own red result
I checked this rather than taking it on trust, because a brand new upgrade lane failing on its first run is equally consistent with the lane being wrong. Two of the three failures carry a terminal mechanism and are platform defects, not lane defects.
The seaweedfs pair fails during template rendering: execution error at (seaweedfs/templates/seaweedfs.yaml:191:6) and (cozy-seaweedfs/templates/naming-guard.yaml:151:6), the naming guard refusing the release because volume names are derived from the release name. That is only reachable by upgrading an existing install, which is exactly the gap this lane closes. tenant-root/tenant-root then fails waiting on it, so that is a consequence rather than a fourth defect.
opensearch-operator fails with Deployment.apps "opensearch-operator-controller-manager" is invalid: spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy type is Recreate. That is the server-side-apply residue case, where the field set by the previous version survives into the new object. Upgrade-only again, and opensearch-rd is a downstream dependency of it, not a separate defect.
The third, vm-instance-test timing out on VirtualMachine/tenant-test/vm-instance-test status: 'InProgress', is a real observed regression signal, but the log does not establish a root cause, so I would not put it in the same category as the other two yet. Worth saying that explicitly rather than counting three equally proven findings.
All three belong in their own changes, so none of them block this one. I also confirmed the lane is genuinely advisory rather than taking the description's word for it: branch protection on main requires only pre-commit and E2E Tests, and this job's check name is Upgrade E2E Test, which is neither.
Non-blocking follow-ups
-
A permanently red advisory check trains reviewers to ignore it, and then it stops working on the day it catches something new. Nothing here records which failures are currently expected, so once this merges a fourth, unrelated failure is indistinguishable from the three known ones. Landing the three fixes first, or recording the known-failing set somewhere the lane prints, would keep it honest.
-
hack/e2e-chainsaw-upgrade/verify/platform/chainsaw-test.yaml:44and:55:comm -13 "$dir/unbound-pv.txt" ... || trueturns a missing baseline file into an empty diff and therefore a pass, so a verify phase run without its seed phase reports healthy having compared nothing. The sibling suite already handles this correctly atverify/vm/chainsaw-test.yaml:47, which fails fast with[ -f "$before" ] || { ...; exit 1; }. Applying the same guard here would make the two consistent. This was raised in the automated review and is still open. -
hack/upgrade-prev-version_test.bats:87: the case named "line match is dot-anchored (1.5 does not match v155.x)" does not test that. I removed thesed 's/\./\\./g'escaping athack/upgrade-prev-version.sh:41and all ten cases stayed green, becausestable_descalready constrains tags tovX.Y.Zand that makes the escaped and unescaped patterns equivalent over the resulting set. The shipped code is correct, only the test overclaims. An earlier review reached the same conclusion. The rest of the suite is not vacuous: unanchoring the stable-tag regex, and starting the minor walk atmininstead ofmin - 1, each turned a case red. -
.github/actions/e2e-download-assets/action.yaml:50:curl -sSL ... -o _out/assets/nocloud-amd64.raw.xzhas no--fail, so an auth failure or 404 gets written into the disk image while curl exits 0, surfacing much later as a confusing decompression error. The behaviour is unchanged from the code this was extracted from, but it now serves two jobs, and the release PR path is precisely the one this lane exists for. Also raised in the automated review and still open. -
The description does not name the costs it accepts: a second full platform install per run on a 32 vCPU runner with a 180 minute ceiling, doubled exposure to install-phase flakiness, and six verify suites to maintain as those apps change. I think the trade is worth making, but it should be stated rather than left implicit. The "Status" section also still says "Draft" and "unit-tests green locally", neither of which holds now.
-
hack/e2e-install-cozystack.batsis also being changed by another open PR, in the worker image catalog region rather than the readiness gate this one extracts. They should not conflict; I am flagging it only so whoever merges second knows to look.
What I checked and found clean
No EXIT or RETURN traps in either new pipeline BATS file or in any of the twelve new Chainsaw suites. hack/e2e-wait-hr-ready.sh uses if kubectl wait ...; then rather than capturing output into a variable, so it avoids the failure mode where an assignment with command substitution under set -e kills the shell before the captured output is ever printed. I checked every command-substitution assignment this diff adds and none has that shape. hack/upgrade-prev-version_test.bats is correctly picked up by the unit target rather than filtered out as an e2e file, and it declares ten cases and executes ten. The tenant Kubernetes suites gate off through an early exit 0 in a single script step and print a visible skip line. The lane runs fixed seed and verify paths with no selector, so unlike the app suite it cannot silently select nothing and report success; the one way it can report a non-failure having run nothing is the skip path in B1. The seed suites really do reuse the existing app manifests, all five relative paths resolve. Documentation is in sync, this diff adds the matching section to the E2E conventions doc.
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
NOT LGTM. Second round on the same head. Two things are new since my previous review, and the rest of that review stands unchanged: a second blocker, the file header in the MariaDB seed suite prescribing the connection method the same file documents as fatal, and a corrected version of the resolver follow-up, where the defect is real but my earlier framing of it was wrong. I also tightened the evidence on the original blocker.
Blockers
B1: broadening the label gate breaks the contract test that guards it, which takes down the whole pipeline
Unchanged from the previous round, restated because it still blocks. .github/workflows/pull-requests.yaml:32 and :454 broaden the gate from github.event.label.name == 'full-e2e' to contains(fromJSON('["full-e2e","upgrade-e2e"]'), github.event.label.name), while hack/promote-gate-contract.bats:122 and :126 pin the old literals. Both grep -cF counts fall to zero, bats-unit-tests fails, Unit & controller tests goes red, Finalize is skipped, and with it both E2E Tests and Upgrade E2E Test.
New evidence this round. This is not an artifact of the branch trailing main. The branch is zero commits behind main, and hack/promote-gate-contract.bats is byte-identical between this head and origin/main, where it still pairs with the narrow expressions at lines 31 and 453. The test and the workflow agree on main, and this PR is what puts them out of step. Attribution is unambiguous.
Fix: update both assertions to the broadened expressions and add one asserting upgrade-e2e is in the allow-list, so the contract keeps guarding the gate instead of being routed around.
B2: the MariaDB seed header prescribes the connection method the same file documents as fatal
hack/e2e-chainsaw-upgrade/seed/mariadb/chainsaw-test.yaml:5 says the canary is written "over 127.0.0.1 from inside the primary pod". Lines 23 and 24 of the same file say the opposite, with a mechanism attached: "connecting to 127.0.0.1 fails (server does not bind loopback) and its retry OOM-kills the nano server container; the primary Service is clean". The code at lines 65 and 70 uses -h mariadb-test-primary, matching the second comment and contradicting the header.
Commit f598c253 ("fix upgrade canary DB access, verified on a live cluster") records this same finding from a dev cluster and changed the code and the step comment, but left the file header on the pre-fix approach. So the header describes the method that commit replaced.
The header is the first thing anyone reads here. Someone simplifying the suite back to what it claims to do reproduces an OOM-kill of the server container, and that surfaces as a timeout that looks unrelated rather than as a connection error, which is the expensive kind to debug. Rewriting the header to match -h mariadb-test-primary is the whole fix. One line of prose, no test needed.
Non-blocking follow-ups
Items 4 and 8 are new this round. The rest are unchanged from the previous review.
-
A permanently red advisory check trains reviewers to ignore it, and then it stops working on the day it catches something new. Nothing here records which failures are currently expected, so once this merges a fourth, unrelated failure is indistinguishable from the three known ones. Landing the three fixes first, or recording the known-failing set somewhere the lane prints, would keep it honest.
-
hack/e2e-chainsaw-upgrade/verify/platform/chainsaw-test.yaml:44and:55:comm -13 "$dir/unbound-pv.txt" ... || trueturns a missing baseline file into an empty diff and so into a pass, and a verify phase run without its seed phase reports healthy having compared nothing. The sibling suites handle this correctly already,verify/vm/chainsaw-test.yaml:47andverify/redisboth fail fast with[ -f "$before" ] || { ...; exit 1; }. The same guard here would make the three consistent. Raised in the automated review and still open. -
hack/upgrade-prev-version_test.bats:87: the case named "line match is dot-anchored (1.5 does not match v155.x)" does not test that. I removed thesed 's/\./\\./g'escaping athack/upgrade-prev-version.sh:41and all ten cases stayed green, becausestable_descalready constrains tags tovX.Y.Z, which makes the escaped and unescaped patterns equivalent over that set. The shipped code is correct, only the test overclaims. An earlier review reached the same conclusion. The rest of the suite holds up: unanchoring the stable-tag regex, and starting the minor walk atmininstead ofmin - 1, each turned a case red. -
hack/upgrade-prev-version.sh:56: a single-component target is silently misparsed instead of rejected.min=$(printf '%s' "$ver" | cut -d. -f2)returns the whole string when there is no second field, sov1givesmaj=1 min=1, the numericcaseguard accepts1:1as well-formed, and the walk resolves the 1.0 line. Confirmed against a synthetic tag set:v1returnsv1.0.9with exit 0. Two- and three-component targets are fine,cutfinds a second field in both. Unreachable from CI, since the workflow requires three components before it sets a target at all, but this is a standalone tool with a ten-case suite and one more fixture would pin it. -
.github/actions/e2e-download-assets/action.yaml:50:curl -sSL ... -o _out/assets/nocloud-amd64.raw.xzhas no--fail, so an auth failure or 404 gets written into the disk image while curl exits 0, surfacing later as a confusing decompression error. Behaviour is unchanged from the code this was extracted from, but it now serves two jobs, and the release PR path is the one this lane exists for. Raised in the automated review and still open. -
The description does not name the costs it accepts: a second full platform install per run on a 32 vCPU runner with a 180 minute ceiling, doubled exposure to install-phase flakiness, and six verify suites to maintain as those apps change. I think the trade is worth making, but it should be stated. The "Status" section also still says "Draft" and "unit-tests green locally", neither of which holds now.
-
hack/e2e-install-cozystack.batsis also being changed by another open PR, in the worker image catalog region rather than the readiness gate this one extracts. They should not conflict. Flagging it so whoever merges second knows to look. -
Both phases write
chainsaw-report.xmlinto the same directory and the workflow always uploads it asupgrade-chainsaw-report, so when the lane fails during seed the artifact is the seed report published under the verify name. The step comment assumes verify ran last, true only on the passing path. Copying per phase, or naming the artifact by the phase that produced it, removes the ambiguity in the case where someone is actually reading it.
Pre-existing, not introduced here
template-injection is disabled tree-wide in .github/zizmor.yml, which is why the new composites' ${{ inputs.sandbox-name }} interpolations into run: pass the gate. The values are sha256 hex so there is no exploitable path in this diff, but the new composite actions enlarge the surface that audit will cover once it is re-enabled. Worth attaching to the existing hardening item. Separately, hack/e2e-chainsaw/mariadb/mariadb.yaml carries hardcoded fixture credentials that predate this PR, and the new suites re-type the password literal in two more places, which argues for sourcing it from the manifest.
The lane's own red result, and what I verified clean
Unchanged from the previous round. Short form: of the three failures the lane surfaced, the seaweedfs naming-guard render failure and the opensearch spec.strategy.rollingUpdate rejection are platform defects reachable only by upgrading an existing install, which is the gap this lane exists to close. The vm-instance-test timeout is a real signal whose root cause the log does not establish, so I do not rank it with the other two. None of the three blocks this PR. The lane is advisory for real: branch protection requires only pre-commit and E2E Tests, and this job's check is Upgrade E2E Test.
Also unchanged. No EXIT or RETURN traps anywhere in the new suites or pipeline BATS files. No command-substitution assignment in this diff has the shape where set -e kills the shell before the captured output is printed. The new unit BATS declares ten cases, executes ten, and is picked up by the unit target. The tenant Kubernetes suites gate off through an early exit 0 and print a visible skip line. The lane has no suite selector, so it cannot silently select nothing, and the only way it reports a non-failure having run nothing is the skip path in B1. All five reused app manifest paths resolve. The migration-stamp assertion is reachable under the isp-full variant the lane selects. The E2E conventions doc gained the matching section in this diff.
| # | ||
| # Reuses the app suite's MariaDB manifest verbatim (DRY). The canary lives in | ||
| # the manifest's `testdb`, written as `testuser` (password from the same | ||
| # manifest) over 127.0.0.1 from inside the primary pod — TCP so the user@%% |
There was a problem hiding this comment.
This header says the canary is written over 127.0.0.1 from inside the primary pod, but lines 23 and 24 of this same file say connecting to 127.0.0.1 fails because the server does not bind loopback, and that the retry OOM-kills the nano server container. The code at lines 65 and 70 uses -h mariadb-test-primary, matching the second comment. Commit f598c253 fixed the code and the step comment after finding this on a live cluster, but left this header on the pre-fix approach, so it now describes the one method known to be fatal here.
|
Aleksei Sviridkin (@lexfrei) both blockers are fixed, plus the corrected resolver finding from your third round — B1, contract test. This one was mine to own: I ran the unit suite before writing that commit rather than after, and B2, MariaDB header. Rewritten to Resolver, per your correction. Reproduced — Body. The red e2e is not this PR. Still open from your non-blocking list: |
Reimplements the intent of #2401 (previous-minor-stable -> current upgrade test) over the post-Chainsaw-migration codebase. The old #2401 was a single 895-line BATS file, now obsolete; this builds a two-phase Chainsaw lane bracketing an external helm upgrade, reusing existing files throughout. Flow (make upgrade-cozystack, in the e2e sandbox): install-previous (bats, prev OCI chart) -> seed (chainsaw, skipDelete) -> helm upgrade to in-tree chart (bats) -> verify (chainsaw, assert-only). DRY over existing files: - 4 composite actions (.github/actions/e2e-{download-assets,prepare,collect, teardown}) shared by both the e2e and new upgrade-e2e jobs; the e2e job is refactored onto them (behavior-preserving). - hack/e2e-wait-hr-ready.sh: the all-HR-Ready gate extracted from e2e-install-cozystack.bats so install and upgrade share one legible gate. - hack/upgrade-prev-version.sh (+ unit test) resolves the baseline version. - seed suites apply the existing app manifests by relative path (no new CRs). Trigger: an upgrade-e2e job in pull-requests.yaml, parallel to e2e, gated on the release label or a maintainer upgrade-e2e label. Advisory / non-blocking. Coverage: postgres + mariadb canary rows, redis/VM volume survival, migration stamp advance, all-HRs-Ready + no-new-unbound-PV. The tenant Kubernetes survival suites (#2931 path) are env-gated OFF (UPGRADE_E2E_TENANT_K8S) as the flakiest surface, pending dev-stand validation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Dev-cluster validation of the seed/verify canary commands surfaced two real bugs, now fixed and re-verified (postgres=3, mariadb=3 rows): - postgres: the seed writes SQL to psql over a heredoc on stdin, but `kubectl exec` without `-i` does not forward stdin, so CREATE/INSERT silently never ran. Add `-i`. - mariadb: the nano server container ships the `mariadb` client (MariaDB 11 dropped `mysql`) and does not bind 127.0.0.1 — connecting there fails and its retry OOM-kills the container. Use the `mariadb` client against the mariadb-test-primary Service (writable primary) from mariadb-test-0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Pre-commit's zizmor audit flags any `>> $GITHUB_ENV` write inside a composite action as a cross-boundary env write (github-env, "may allow code execution") — even the identical line passes in a workflow file. Move the SANDBOX_NAME write back into each job as a one-liner (the pre-composite, zizmor-clean form) and have the e2e-prepare composite only copy the workspace + run prepare-env. GITHUB_JOB keeps the e2e and upgrade-e2e sandboxes distinct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
…dfs baseline The etcd v1alpha2 adoption snapshot cannot succeed in an e2e sandbox: it needs a trusted-cert (ACME) external S3 endpoint, which a sandbox (example.org, no DNS/ACME, self-signed in-cluster cert) cannot provide — matching the dev10 findings. Set migrations.etcdAdoptSkipBackup=true in the platform Package so migration 50 runs the real adoption without the infeasible snapshot. Keep seaweedfs on the root tenant (the historical default install-cozystack.bats also uses) for a realistic baseline, but drop the fragile cozy-backups-creds wait — the adoption no longer depends on the seaweedfs S3/bucket/creds chain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The composites extracted here were written against the pre-78fc3f917 e2e job, which copied the checkout to /tmp/$SANDBOX_NAME, ran every step from there and removed it on teardown. main has since dropped that workspace: e2e is a single job on an ephemeral runner, so there is no cross-job rendezvous and the checkout is never reused. Resolving the rebase in favour of the composites would have reintroduced it, including the unguarded `rm -rf /tmp/$SANDBOX_NAME` that commit deliberately deleted rather than guarded — when an upstream step fails the unconditional `Set sandbox ID` is skipped, SANDBOX_NAME stays empty, and the `if: always()` removal expands to `rm -rf /tmp/`. Drop the Prepare/Remove workspace steps and the `cd /tmp/$SANDBOX_NAME` prefixes from e2e-prepare, e2e-collect and e2e-teardown, and point the collect and chainsaw-report upload paths at _out/. Same treatment for the upgrade-e2e job's own two `cd` prefixes. Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
main gained a `labeled` pull_request trigger whose plan/resolve guards
discard every label event except `full-e2e`. This lane's documented opt-in
("any PR a maintainer opts in with the upgrade-e2e label") therefore did
nothing after the rebase: adding the label fired a `labeled` event, `plan`
skipped, `finalize` skipped with it, and upgrade-e2e never satisfied its
needs. Before the rebase there was no `labeled` trigger at all, so the label
was only ever read on push — the gap appeared with the new base, not here.
Admit `upgrade-e2e` alongside `full-e2e` in both allowlists so labelling an
open PR starts a run, and use !cancelled() rather than always() in the job
gate, matching the sibling `e2e` job: a cancelled run must not go on to
occupy a 32-vCPU runner for up to 180 minutes.
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The previous commit broadened the plan and resolve_assets label gates to admit upgrade-e2e, but hack/promote-gate-contract.bats pins those two expressions as exact grep -cF literals. Both counts fell to zero, bats-unit-tests failed, and because "Unit & controller tests" gates finalize, the required E2E Tests check and this PR's own upgrade lane were both skipped — the commit meant to make the label start a run stopped every run instead. I broke this by running the unit suite before writing that commit rather than after; pre-commit does not cover bats-unit-tests, so it went unnoticed. Update both assertions to the broadened expressions, and add a case pinning the label end to end: admitted by the `labeled` allow-list in both plan and resolve_assets, honoured by the job's own condition, and named "Upgrade E2E Test" so it cannot collide with the required "E2E Tests" context and stop being advisory. Note the label is double-quoted inside the JSON allow-list — matching the single-quoted form counts zero and asserts nothing. Verified non-vacuous: narrowing the plan gate back to the full-e2e-only literal turns exactly one test red. Also correct the MariaDB seed header, which still prescribed the 127.0.0.1 connection that commit f598c25 replaced — the same file documents that path as OOM-killing the nano server container. Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
`cut -f2` prints the whole line when that line carries no delimiter, so a single-component target like "v1" came back with min=1 and was resolved as if it were v1.1 — walking down to the v1.0 line and printing a real-looking baseline with exit 0. The lane would then install that baseline and report on an upgrade path nobody asked for, which is worse than failing. Pass -s so the delimiter-less line is suppressed, min comes back empty, and the existing parse guard rejects it. Two-component targets are unaffected: the guard rejects a missing minor, not a missing patch, and v1.6 still resolves. In practice the lane only ever passes "" or a target derived from a release-X.Y.Z branch, so this was latent rather than live. Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The case I just added was vacuous. _fixture ships no v1.0.x or v1.1.x tag, so the buggy reading of "v1" as v1.1 walked down to an empty v1.0 line and exited non-zero for the wrong reason — the assertion held whether or not the defect was present, which the mutation run showed: reverting -s left all eleven green. Give the case its own tag set containing v1.0.9, so the buggy path has a real baseline to return with exit 0 and the case can only pass because the target is refused. Reverting -s now fails this case and only this case. Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The upgrade-e2e label opt-in was restored in "make the upgrade-e2e label opt-in actually start a run" by naming the label in `plan`'s and `resolve_assets`' allowlists. Both gates still exist and that commit still applies, but main has since grown two more places that read the same label, and the opt-in is not actually live until both agree. The concurrency key routes a `labeled` event into a separate `-label` group, documented as the exact complement of `plan`'s guard so that only a run publishing nothing is moved aside. Once `plan` admits upgrade-e2e the complement is no longer exact: the run publishes, but sat in its own group, which is two live publishers of the `E2E Tests` context on one head SHA — the case the comment there warns produces a later green that erases a real failure. `e2e-report` carries the same `labeled` guard for the opposite reason, to stay in lockstep with `plan`. The hazard it documents is running while `plan` skipped; the hazard here is the reverse. `plan` overwrites `E2E Tests` with `pending` at the start of every run it executes and only `e2e-report` concludes it, so admitting the label to one and not the other opens a pending required status on an upgrade-e2e label and never closes it, leaving the PR unmergeable until someone pushes again. The lane stays advisory. `e2e-report` does not gain upgrade-e2e in `needs`, so the lane's result never moves the verdict, and on an upgrade-e2e-labelled run the sibling `e2e` job still supplies the real one. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
6c78739 to
48ae5be
Compare
What this PR does
Adds automated release upgrade testing — the "previous latest minor stable release → current version" path — rebuilt for the Chainsaw-based E2E suite (#2826). It installs the previous Cozystack, seeds real workloads with canary data, upgrades the platform to the build under test, then verifies workloads survived, data is intact, every HelmRelease reconciled, PVs stayed Bound, and the migration stamp advanced.
Supersedes #2401, which predates the BATS→Chainsaw migration and is now a single conflicting 895-line BATS file. Its useful pieces (previous-version resolution, the isp-full platform Package, the canary SQL, the CrashLoop/unbound-PV baseline diff) are re-expressed here.
Architecture
Chainsaw cannot pause mid-test for an external
helm upgrade, so the flow is two Chainsaw phases bracketing the upgrade, driven bymake upgrade-cozystackinside the e2e sandbox:DRY over existing files
.github/actions/e2e-{download-assets,prepare,collect,teardown}) for the sandbox lifecycle, shared by both thee2ejob and the newupgrade-e2ejob. The existinge2ejob is refactored onto them (behavior-preserving).hack/e2e-wait-hr-ready.sh— the all-HelmReleases-Ready gate, extracted frome2e-install-cozystack.batsso install and upgrade share one legible, fail-fast gate.hack/upgrade-prev-version.sh(+upgrade-prev-version_test.bats, 10 cases) — resolves the baseline version from git tags.applythe existing app manifests by relative path — no re-authored CRs.Trigger
A new
upgrade-e2ejob inpull-requests.yaml, a sibling ofe2ethat runs in parallel (no added critical-path wall-clock). It runs on the release-promotion PR (releaselabel) or any PR a maintainer opts in with the newupgrade-e2elabel. Advisory / non-blocking — its own check, gates nothing.Coverage
Always-on: PostgreSQL + MariaDB canary rows, Redis/VM volume survival, migration stamp advance, all-HRs-Ready + no-new-unbound-PV, aggregated API available. The tenant-Kubernetes survival suites are env-gated OFF (
UPGRADE_E2E_TENANT_K8S) as the flakiest surface (nested KVM + DRBD + tenant-worker image import), pending validation on a dev cluster.Status
Ready.
actionlint,zizmorand the full unit suite pass, andhack/promote-gate-contract.batsnow pins both halves of theupgrade-e2elabel opt-in — the allow-list that admits thelabeledevent and the job's own condition — so the gate cannot drift from the test that guards it again. The canary access paths were fixed against a live dev cluster (f598c253); the tenant-Kubernetes verify suites remain env-gated off behindUPGRADE_E2E_TENANT_K8Sas the flakiest surface, pending their own validation run.The
upgrade-e2ejob runs only on a PR carrying theupgrade-e2eorreleaselabel. Its check isUpgrade E2E Test, which is not a required context — branch protection requires onlypre-commitandE2E Tests— so the lane is advisory and gates nothing.The lane is currently red, and that is the lane working: it found upgrade-only defects that no other suite reaches. Two are confirmed platform bugs with a terminal mechanism — the seaweedfs naming guard refusing a 1.5.x→1.6 tenant upgrade, and opensearch-operator's
spec.strategy.rollingUpdatesurviving into atype: Recreatedeployment under server-side apply. A third,vm-instancetiming out atInProgress, is a real signal whose root cause the log does not establish, so it is not in the same category. All three belong in their own PRs.Costs this accepts, stated rather than left implicit: a second full platform install per run on a 32-vCPU runner with a 180-minute ceiling, doubled exposure to install-phase flakiness, and six verify suites to maintain as those apps change.
Summary by CodeRabbit
etcdAdoptSkipBackupmigration flag to bypass the pre-adoption safety snapshot.