You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 1bab585
Browse filesBrowse the repository at this point in the historyBrowse files
docs(e2e): describe the per-suite runner and the pod-leak guard
The agent guide, the suite README and a workflow comment said the E2E
step is a single chainsaw invocation; it is now one per suite, and a
suite whose pods outlive its cleanup fails. Say how a pod is charged
to a suite, what the budget is, and how the allowlist is kept honest.
The workflows' failure line now points at LEAKED-PODS as well, since a
run that failed only on a leak has no assertion diff to point at.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
Copy file name to clipboardExpand all lines: docs/agents/e2e-testing.md
+4-3Lines changed: 4 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -14,7 +14,7 @@ Every convention below follows from that finding: **fail fast, fail loud, make t
14
14
15
15
### 1. No retries on deterministic steps; retry only pure infrastructure
16
16
17
-
-`Run E2E tests` and `Install Cozystack` run **once**. On failure they print `❌ ... (no retry — see diagnostics below)` and dump state. Do not reintroduce a retry loop around them. The E2E step is a single `chainsawtest`invocation — there is no per-app retry loop to bring back, because Chainsaw already polls each assertion to its own timeout.
17
+
-`Run E2E tests` and `Install Cozystack` run **once**. On failure they print `❌ ... (no retry — see diagnostics below)` and dump state. Do not reintroduce a retry loop around them. The E2E step runs each suite once through `hack/e2e-chainsaw-run.sh` (one `chainsaw test`per suite directory, for the leak guard in §8) — there is no per-app retry loop to bring back, because Chainsaw already polls each assertion to its own timeout.
18
18
-`Prepare environment` keeps its 3× retry — and *only* it — because it is pure infrastructure (image pull, node container start, network) where a transient runner hiccup is a genuine flake with no application logic involved. The retry tears the compose project down between attempts, because a half-started project the next attempt adopts is not a fresh try.
19
19
- Rule of thumb: retry is justified only for a step that contains **no product or test logic**.
20
20
@@ -92,7 +92,7 @@ A parent HelmRelease that hit its wait timeout, uninstalled, and reinstalled is
92
92
- **Every escalation names its cause on stderr.** "Run everything" is the same list of suite names whichever rule produced it, so a reason line is the only thing that distinguishes the branches that can reach it, and without one the answer to "why did this pull request run the whole suite" has to be re-derived by hand from the diff. Four of them used to be silent — the shared `hack/e2e-chainsaw/_lib/` and `.chainsaw.yaml` case, the `full_suite_pattern` match, a `packages/` path no PackageSource claims, and the empty-selection backstop — and `full_suite_pattern` is the commonest cause of a full run by a wide margin, so the usual answer was the one the log never carried. Each line names the path or the sources that caused it, and a unit test asserts each message, because a reason that regresses to silence changes no selection and is invisible to every other test. The lines go to **stderr** and must stay there: stdout is the suite list and both lanes parse it, so a reason line on stdout would be read as a suite name.
93
93
- **The one path the fall-through cannot catch is a new e2e workflow.** `inert_config_pattern` makes `.github/` inert as a directory, and the workflows that run the suite are exempted from it by name in `full_suite_pattern`. A workflow added under `.github/workflows/` therefore reads as inert until someone adds it to that enumeration, and the fall-through never sees it. That is why the list carries a `rg -l test-chainsaw .github/workflows/` reminder: the enumeration is the guard, not a convenience.
94
94
- The `full-e2e` PR label forces the whole suite.
95
-
- The selection is passed to `make test-chainsaw CHAINSAW_SUITES="<names>"`, which runs `chainsaw test <names>` from `hack/e2e-chainsaw/` (empty = the whole suite).
95
+
- The selection is passed to `make test-chainsaw CHAINSAW_SUITES="<names>"`, which runs `hack/e2e-chainsaw-run.sh <names>` from `hack/e2e-chainsaw/` (empty = every directory there). The runner hands chainsaw one suite at a time and runs the tests a single `chainsaw test <names>` would discover, no more and no fewer.
96
96
- The coverage gap TIA opens on PRs is closed at release-cut time: the "Prepare release" commit bakes image digests into `packages/core/` (and other packages), which TIA escalates to the **full** suite — so the release PR exercises the whole suite against the commit the tag will point at.
97
97
98
98
When adding a new app package, confirm `select-e2e.sh` maps it correctly (it has a unit test, `hack/select-e2e_test.bats`). A new app whose suite directory does not yet exist will not be selected — add `hack/e2e-chainsaw/<app>/chainsaw-test.yaml` first.
@@ -111,7 +111,8 @@ Be precise about TIA's scope — it is narrower than "skip E2E for unrelated PRs
111
111
### 8. Failure must be self-explanatory — diagnostics are first-class
112
112
113
113
- On install failure, the CI step dumps `kubectl get hr -A -o wide`, a `describe` of each non-Ready HR, and sorted events, all under collapsible `::group::` blocks.
114
-
- On E2E failure, the CI step dumps `kubectl get hr -A`, sorted events, and COSI bucket/claim/access readiness columns (those CRDs ship no printer columns, so plain `get` shows only NAME/AGE). The `chainsaw test` run is one step, so this dump is at run level; Chainsaw's own per-`Test` `catch` blocks add scoped events/describe/podLogs for the failing suite.
114
+
- On E2E failure, the CI step dumps `kubectl get hr -A`, sorted events, and COSI bucket/claim/access readiness columns (those CRDs ship no printer columns, so plain `get` shows only NAME/AGE). The whole suite run is one CI step, so this dump is at run level; Chainsaw's own per-`Test` `catch` blocks add scoped events/describe/podLogs for the failing suite.
115
+
- **A suite whose pods outlive it fails where it leaked, not where the memory ran out.** A suite can pass and still leave its workloads running — brokers of Kafka releases whose application was deleted, a TenantControlPlane stuck on its finalizer — and the first thing to notice used to be a later suite failing on `Insufficient memory`, hours on and under the wrong name. Chainsaw v0.2.15 has no hook that runs after a test's cleanup (its Configuration carries `error.catch`, which runs only on failure), so `hack/e2e-chainsaw-run.sh` runs one `chainsaw test` per suite directory and checks between them, with `hack/e2e-chainsaw/_lib/leak-guard.sh`. A pod is charged to the suite when it is not terminal, did not exist before the suite, and the root of its ownerReferences chain was created at or after the suite started on the apiserver's clock; a platform pod replaced mid-suite still traces back to an object the install created, so it is not charged. Charged pods get the default `timeouts.cleanup` of `.chainsaw.yaml` (5m), counted from when `chainsaw test` returns and so after any per-suite cleanup override has been spent, to disappear; past that the run prints `LEAKED-PODS suite=<dir>` with one line per pod and its root, raises a `leaked-pods` error annotation, and adds a failed `leak-guard` case to `chainsaw-report.xml`. Every suite that leaves nothing behind logs how long it took to clear, and leftovers of a suite that already failed are reported once as a warning, without the wait. The wait has three outcomes, and nothing is retried. A timeout or a spent budget gives the verdict from the last good pod list, including when the re-list after it fails: a leak fails the suite. Any other error on a wait or a re-list, before the budget is spent and before any timeout, stops the waiting and is reported as `LEAKED-PODS suite=<dir> count=N inconclusive="<error>"`, a warning rather than a failure, so an apiserver blip cannot turn into a leak red. A failed first reading after a suite leaves the suite unchecked, and it fails with a `leak-guard` annotation and a report case saying the cluster could not be read, not that it leaked. A suite with a known, filed leak is listed in `hack/e2e-chainsaw/_lib/leak-allowlist.txt` as `<suite-dir> <issue-ref>`; its leak prints as a warning and does not fail. The exemption covers the whole suite, so the PR that fixes the issue deletes the line, and an entry naming a directory that is not a suite stops the run before the first suite.
115
116
- Inside a suite, attach scoped diagnostics with a top-level `catch:` block (`events`, `describe`, `podLogs`, `get`) rather than letting a bare assert failure through — see `hack/e2e-chainsaw/bucket/chainsaw-test.yaml` and `hack/e2e-chainsaw/harbor/chainsaw-test.yaml`. This replaces the BATS `... || { echo …; kubectl get/describe …; false; }` idiom.
116
117
- `make collect-report`→ `hack/cozyreport.sh` uploads a full `cozyreport.tgz` (operator/Flux/cert-manager logs, LINSTOR, Kamaji, Talos dmesg, COSI YAML) on every run, and the Chainsaw JUnit report (`chainsaw-report.xml`) is uploaded as its own artifact.
117
118
- **Which machine a run landed on is recorded before the sandbox container exists**, in `sandbox-host/runner-identity.txt`: the OCI shape, both region keys, the availability and fault domain, the numbers of the `shapeConfig` sizing object, and the runner VM's own kernel release, online CPU count and processor model. A lane asks for a runner class rather than for a machine — a flex shape is filled from whatever capacity the pool has, and the region is a preference with a fallback — so without this a red run and a green one cannot be compared by the hardware they ran on, which is the first question a flake that only some runs have will raise. `hack/e2e-runner-identity.sh` takes the read on the runner VM, from `packages/core/testing/Makefile`'s `apply` target, because the metadata service answers on a link-local address the sandbox container has no guaranteed route to, and an "unreachable" answered from in there would look exactly like a capture that had tried. Every e2e lane reaches the sandbox through `make prepare-env`, so the record is taken once per run in each of them, on the passing path as well as the failing one. It goes to the job log as well as to the file, and the log copy is not a convenience: a run whose sandbox never came up has no `cozyreport.tgz` at all, so on that one path the log is the only place the record exists.
Copy file name to clipboardExpand all lines: hack/e2e-chainsaw/README.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
The Cozystack E2E app suite, written for [Kyverno Chainsaw](https://github.com/kyverno/chainsaw) — a declarative, Kubernetes-native E2E test framework (CNCF, part of the Kyverno project; successor to KUTTL).
4
4
5
-
This directory replaces the per-app BATS suite that used to live in `hack/e2e-apps/*.bats`. The CI `e2e` job runs `chainsaw test hack/e2e-chainsaw/` after `make install-cozystack`; cluster bootstrap (`hack/e2e-prepare-cluster.bats`, `hack/e2e-install-cozystack.bats`) and the OpenAPI checks (`hack/e2e-test-openapi.bats`) remain BATS.
5
+
This directory replaces the per-app BATS suite that used to live in `hack/e2e-apps/*.bats`. The CI `e2e` job runs every suite under `hack/e2e-chainsaw/` through `hack/e2e-chainsaw-run.sh` after `make install-cozystack`; cluster bootstrap (`hack/e2e-prepare-cluster.bats`, `hack/e2e-install-cozystack.bats`) and the OpenAPI checks (`hack/e2e-test-openapi.bats`) remain BATS.
6
6
7
7
## Layout
8
8
@@ -72,4 +72,4 @@ chainsaw test postgres/
72
72
73
73
## CI integration
74
74
75
-
The `e2e` job in `.github/workflows/pull-requests.yaml` runs `make -C packages/core/testing test-chainsaw` (which execs `chainsaw test hack/e2e-chainsaw/` inside the e2e sandbox) and uploads `chainsaw-report.xml`. The sandbox image (`packages/core/testing/images/e2e-sandbox/Dockerfile`) ships the `chainsaw` binary alongside `kubectl`/`helm`/`mc`/`jq`/`nc`.
75
+
The `e2e` job in `.github/workflows/pull-requests.yaml` runs `make -C packages/core/testing test-chainsaw` (which execs `hack/e2e-chainsaw-run.sh` inside the e2e sandbox) and uploads `chainsaw-report.xml`. The runner calls `chainsaw test` once per suite directory, fails a suite whose pods outlive its cleanup by more than the default `timeouts.cleanup` (`LEAKED-PODS suite=<dir>` in the log; see `docs/agents/e2e-testing.md` §8 and `_lib/leak-allowlist.txt` for known, filed leaks), and merges the per-suite reports into that one file. A local `chainsaw test` as shown above runs the same tests without the leak check. The sandbox image (`packages/core/testing/images/e2e-sandbox/Dockerfile`) ships the `chainsaw` binary alongside `kubectl`/`helm`/`mc`/`jq`/`nc`.
0 commit comments