294 Commits
Author SHA1 Message Date
Aleksei Sviridkin ba0b1ac6ef fix(e2e): validate the leak allowlist against every suite layout (#4972)
`lg_allowlist_validate` in `hack/e2e-chainsaw/_lib/leak-guard.sh`
accepted only `<suite>/chainsaw-test.y*ml`, while the selectors already
see step-file suites and a Test nested below the suite directory
(#4630). A suite in one of those layouts could not be put on the leak
allowlist at all: validation failed and the run stopped before the first
suite.

- `lg_suite_has_test` validates an allowlist entry with the rule
`hack/select-e2e.sh` enumerates suites with: `chainsaw-test.yaml`,
`chainsaw-test.yml`, or a numbered step file, anywhere in or below the
suite directory, with the selector's type test (a step-file name counts
unless it is a directory, a `chainsaw-test.y*ml` counts whatever its
type, so Chainsaw reports a directory by that name); a parked
`chainsaw-test.yaml.disabled` matches none. Both sides carry a comment
pointing at the other so they move together.
- `hack/e2e-chainsaw/README.md` describes the three layouts once, under
Layout, and lists `kubernetes-proxmox` as the parked suite.
`docs/agents/e2e-testing.md` points at it.
- Unit tests: every layout is accepted, including a `chainsaw-test.yaml`
that is a directory; a parked, empty, or not-a-step-file directory is
refused, including `1a-install.yaml` (passes the `find` glob, not the
step-file regex) and a directory named `01-install.yaml`.

Left as they are, with the reasons:
- `docs/agents/e2e-testing.md` calling `kubernetes-proxmox` the one
parked suite is correct on main now: `hack/e2e-chainsaw/backup/` was
removed in 9cc3a9e45.
- The top-level globs in `hack/e2e-chainsaw-run.sh` are wider than
Chainsaw's step-file pattern, but they only refuse a test at the top
level and fail closed.
- A file under `examples/backups/<other>/` that matches no suite is
documented in `select-e2e.sh` as a docs-only demo that stays ignored.

Verified with Bats and `hack/cozytest.sh` on
`hack/chainsaw-leak-guard_test.bats` (34 tests) and
`hack/select-e2e_test.bats` (65 tests; one case, "a path owned by
several sources counts as covered if any one is", fails on my machine on
main as well, from the local yq build, and does not touch this change).

Closes #4659


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

## Summary by CodeRabbit

* **Documentation**
* Clarified that Chainsaw suites may use `chainsaw-test.yaml` or `.yml`,
numbered step files, or either layout in a subdirectory.
* Updated guidance on suite selection and noted that disabled test files
are not selected or allowlisted.
* Identified the parked Kubernetes Proxmox suite and its workflow
availability.
* **Bug Fixes**
  * Suite validation now recognizes tests in the documented layouts.
* Validation rejects empty directories, parked test files, invalid
step-file names, and directories named like step files.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-29 20:00:50 +03:00
Aleksei Sviridkin 013b166967 fix(e2e): read the KubernetesNodes pool release in the HelmRelease guard (#4970)
`cozy_guard_all_helmreleases` in
`hack/e2e-chainsaw/_lib/run-kubernetes.sh` reads the parent
`kubernetes-<test>` release and the addons selected by the
`kubernetes-<test>-` prefix. The worker pool is
`kubernetes-nodes-<test>-md0`, an Application HelmRelease the aggregated
apiserver stamps with `RetryOnFailure` like the parent, and it sits on
the node-join path through the talos-reconcile Job it renders. Its name
does not start with the addon prefix, so a failed action on it was never
read, on the passing path or in the failure-path report.

This reads it on both paths, with the `required` strategy check the
parent gets, since the apiserver stamps both. The pool name is derived
from the parent (`cozy_pool_helmrelease_name`), the way the addon prefix
is, so the two cannot drift. The failure-path report checks that the
release exists before reading it, with `get hr --ignore-not-found -o
name`: a run whose pool apply failed has no pool release (exit 0, empty
output), and the report says so instead of reporting a failed read. For
that run to reach the report at all, `run_kubernetes_test` now arms the
exit handler (`_tenant_failure_on_exit`, with
`CURRENT_TENANT_PARENT_HR`) right after the parent `Kubernetes` apply
instead of after the `KubernetesNodes` apply: before, a failing pool
apply exited under errexit with no handler installed, and the parent
went unclassified. The snapshot half still no-ops until the tenant
kubeconfig is set. A non-zero exit (a timeout, a kill, a Forbidden) is
reported as the failed read it is, through `_cozy_guard_read_failure`,
never as absence. The pool reads are gated on the report's own deadline
as well as the phase clock, like the addon listing.

Unit tests in `hack/run-kubernetes-remediation_test.bats` cover a
failing pool release on the passing path, a clean one (the composite
returns zero), an empty strategy on the pool, the report with and
without the pool release, a cut-off existence read, and a pool read past
the report budget; the ordering pin now also requires the handler to be
armed before the pool apply. The kubectl stub gained an existence read:
an `ABSENT` marker prints nothing and exits 0 under `--ignore-not-found`
(NotFound without it, as kubectl does), and the failure markers apply to
it as `<name>.exists`.

Verified with `hack/cozytest.sh` and Bats on
`hack/run-kubernetes-remediation_test.bats` (51 tests),
`hack/run-kubernetes-node-join_test.bats` and
`hack/remediation-guard.bats`.

Closes #4737


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

## Summary by CodeRabbit

* **Bug Fixes**
* Kubernetes release checks now include worker-pool releases alongside
the parent release and add-ons, and flag missing or invalid install
strategies and remediation failures.
* Failure reports distinguish worker-pool releases that are absent from
those whose status cannot be read. Existing releases are assessed
against the phase gate and report time budget; when the budget is
exhausted, skipped checks are identified.
* Failure handling now begins immediately after the parent resource is
applied, covering errors that occur while applying the worker pool.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-29 18:36:58 +03:00
Marian Koreniuk 75bb70e58d fix(e2e): arm the tenant failure handler before the pool apply
The exit handler was armed after the KubernetesNodes apply, so a failing
pool apply exited under errexit with no handler installed: the parent
went unclassified, and the report's "pool release does not exist" line
described a run that could never reach it. Arm it right after the parent
Kubernetes apply instead. The snapshot half still no-ops until the
tenant kubeconfig is set, so the only change is that a failed pool apply
now gets the parent classified and the absent pool reported.

The ordering pin now also requires the handler to be armed before the
pool apply.

Assisted-by: LLM
Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
2026-10-29 12:34:22 +01:00
Aleksei Sviridkin 048c029e29 fix(e2e): keep the Proxmox tenant kubeconfig per run and remove it (#4971)
`hack/e2e-chainsaw/_lib/run-proxmox.sh` wrote the tenant's super-admin
kubeconfig to `$TMPDIR/proxmox-e2e-tenant.kubeconfig` with the process
umask and never removed it, so it outlived the run on the self-hosted
runner, world-readable under the usual umask. `dump_diagnostics` then
read whatever sat at that path, so a run that failed before
`fetch_tenant_kubeconfig` reported the previous run's tenant as its own.

- The file now lives under `RUNNER_TEMP`, which the runner exports into
every step and empties between jobs; `COZY_PROXMOX_WORKDIR` overrides it
for a local run, and `TMPDIR` is the last fallback. The workflow sets no
path of its own: the `runner` context is not available in job-level
`env`.
- `fetch_tenant_kubeconfig` writes it under `umask 077` in a subshell,
since the workflow's `umask 077` covers only the staging step.
- `apply-tenant`, the suite's first step, removes a leftover before
doing anything else, so diagnostics in this run can only see this run's
file.
- The suite's `finally` removes it, before the `keep_tenant` exit, and
the workflow's "Remove staged credentials" step removes it again for a
run killed before `finally`.

Verification: `sh -n` and shellcheck on the script; actionlint on
`e2e-proxmox.yaml` (only the custom `proxmox` runner label is reported,
as on main). `hack/proxmox-e2e-fixture_test.bats` has three new cases
with a stub `kubectl`: the fetched file has no group or other bits under
`umask 022`, `apply-tenant` removes a planted leftover, and the suite's
`finally` (run out of the Chainsaw file on the `keep_tenant` path)
removes the file, so `diagnostics` after it prints no tenant section.
Each goes red with its line (`umask 077`, the leftover `rm`, the
`finally` `rm`) removed. All `hack/proxmox-*.bats` pass (fixture 9,
teardown contract 8, step1b 4, talos template 12), also under
`hack/cozytest.sh`. Not run on a Proxmox stand: the only stand I have
cannot dispatch the workflow at the moment, so the workflow steps
themselves are unexercised beyond actionlint.

Closes #4806


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

## Summary by CodeRabbit

* **Chores**
* Proxmox end-to-end test runs store tenant credentials in a temporary
work directory and create kubeconfig files with restricted permissions.
* Stale tenant kubeconfig files are cleared before a new tenant fixture
is applied. Cleanup removes the file even when tenant data is retained
or a run is interrupted, preventing credentials from appearing in later
diagnostics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-29 11:31:31 +03:00
Aleksei Sviridkin 040ae377ae test(kubernetes): name the Proxmox fixture's node pool <cluster>-<pool> (#5000)
## What this PR does

The Proxmox suite (`hack/e2e-chainsaw/kubernetes-proxmox/`) has now run
for the first time on a real self-hosted stand: a dedicated Cozystack
v1.7.0-rc.1 management cluster on Proxmox VE, with the runner CageForge
offered to host in cozystack/community#98. The suite failed before it
reached the hypervisor, because the fixture and the chart disagree on
what a node pool may be called.

`kubernetes-nodes` names its release `kubernetes-nodes-<CR name>` and,
since fd14126be, refuses to render one that does not start with
`kubernetes-nodes-<spec.cluster>-`
(`packages/apps/kubernetes-nodes/templates/_helpers.tpl`). The fixture,
added later in 7500a9fba, names its `KubernetesNodes` `md0`. On the
stand that gave:

```
Helm install failed for release tenant-e2eproxmox/kubernetes-nodes-md0 ...:
kubernetes-nodes: release name "kubernetes-nodes-md0" must start with "kubernetes-nodes-k8s-e2e-"
— name the KubernetesNodes CR <cluster>-<pool> (cluster="k8s-e2e")
```

No `Machine` was ever created. The `tenant-and-cluster` step passed its
`ProxmoxCluster` assert in 20 seconds and then waited out the 20-minute
`Machine` assert.

- The fixture names the pool `k8s-e2e-md0`. Nothing else in the suite,
`run-proxmox.sh` or the workflow refers to the pool by name.
- `hack/proxmox-e2e-fixture_test.bats` gets a case that reads
`metadata.name` and `spec.cluster` of the fixture's `KubernetesNodes`
and fails unless the name is `<cluster>-<pool>`. It is POSIX sh like the
rest of the file, so `hack/cozytest.sh` can still run it.

### Testing

- `bats hack/proxmox-e2e-fixture_test.bats` (bats 1.14.0, the CI
commit): 7/7. The new case was checked to fail against the old fixture:
`FAIL: KubernetesNodes md0 must be named k8s-e2e-<pool>`.
- `hack/check-commit-trailers.sh upstream/main..HEAD`: OK.
- Lane, before the fix (fork `main` @ aed7fa896):
https://github.com/themoriarti/cozystack/actions/runs/39402535098. Red
on the `Machine` assert, as above; the teardown removed the tenant and
left no VM on the hypervisor.
- Lane, with this commit:
https://github.com/themoriarti/cozystack/actions/runs/39442983145. The
pool release `kubernetes-nodes-k8s-e2e-md0` installs, the `Machine` is
created, capmox clones the worker from the template and boots it, and
the worker joins the tenant and goes `Ready` (Talos v1.13.6). The run is
still red, one step later and for a known reason: the tenant's CCM
crash-loops on `pccm:v0.15.0` with `panic: version string "0.15.0"
doesn't match expected regular expression: "^v(\d+\.\d+\.\d+)"`, so the
node keeps `node.cloudprovider.kubernetes.io/uninitialized` and no
providerID, and the `Machine` assert reports `Provisioned`, expected
`Running`. That is #4917. The teardown removed the tenant, the worker VM
and its cloud-init ISO.

Two earlier runs on the same commit, 39406900563 and 39409765857,
stopped at stand configuration rather than at this repository: capmox's
token could not see an ISO storage, and the stand's `publishing.host`
did not resolve from the fixture's public resolvers. Both are fixed on
the stand. They are listed below because the lane does not say a stand
needs them.

### Found on the way, not changed here

- **What a stand must provide**, beyond the `PROXMOX_*` variables:
- an `iso`-content storage on which capmox's token holds
`Datastore.AllocateTemplate` (to upload the cloud-init ISO) and
`Datastore.Allocate` (to delete it with the VM). go-proxmox uploads to
the first `iso` storage the token can see; without delete rights the
`Machine` never finishes deleting.
- a `publishing.host` whose `<cluster>.<tenant>.<host>` resolves through
the fixture's own resolvers (`8.8.8.8`, `1.1.1.1`) to a root ingress
with SSL passthrough: on the Proxmox substrate that name is the tenant's
`controlPlaneEndpoint`.
- **`COZY_PVE_ADDRESSES/GATEWAY/PREFIX/BRIDGE/POOL`** are not passed by
the workflow. Unset, they default to `vmbr50` and `10.0.0.180-185`. This
stand sets them in the runner's environment.
- **The fixture's `templateTags`** (`capmox-template, cozystack, talos,
talos-v1.13.6`) are not the set that `hack/proxmox-talos-template.sh`
writes (`cozystack, schematic-…, talos, talos-v…`). capmox matches tags
as a set, so a template built by the script is not selected. This stand
tags its template to match the fixture.
- **Teardown reports success once the CRs are deleted.** It does not
wait for the VMs to be gone; on this stand a worker VM outlived its
`Machine` for hours while capmox retried a forbidden delete.

The CCM fix itself is #4917. Once a release carries it, this lane should
get past the `Machine` assert.

The lane stays manual. It has no `push` or `pull_request` trigger, and
its nightly schedule is a no-op unless `PROXMOX_NIGHTLY` is set, which
it is not on this stand. The runner is registered to a fork whose only
collaborator is its owner. Running it from cozystack/cozystack needs a
runner group restricted to this workflow and a protected environment,
which is a follow-up for community#98, not this PR.

### Screenshots

Not a UI change.

### Downstream repositories

- [x] No downstream repository is affected by this change

The diff touches one fixture under `hack/e2e-chainsaw/` and one bats
file. It moves or renames nothing under `hack/`, changes no make target
and no node prerequisite.

### Release note

```release-note
NONE
```


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

## Summary by CodeRabbit

- **Bug Fixes**
- Updated the Kubernetes node pool fixture to use the required
cluster-prefixed naming convention, improving compatibility with
Kubernetes node validation.

- **Tests**
- Added validation to confirm that Kubernetes node fixtures include the
required cluster reference and use the expected cluster-based name
format.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-29 11:19:21 +03:00
Marian Koreniuk cd983d416e fix(e2e): take the selector's type test in the allowlist scan and pin its regex
lg_suite_has_test dropped every directory with `! -type d`, but the
selector keeps a chainsaw-test.y*ml whatever its type, so Chainsaw can
fail loudly on a directory by that name. An allowlisted suite in that
shape failed validation and stopped the run before Chainsaw reported the
real error. Use the selector's own `\( ! -type d -o -name
chainsaw-test.yaml -o -name chainsaw-test.yml \)`, which still drops a
directory with a step-file name.

The step-file regex behind the find glob was never reached by a test:
install-01.yaml is already filtered by the glob. 1a-install.yaml passes
the glob and not the regex, so it pins the regex; a directory named
chainsaw-test.yaml and one named 01-install.yaml pin the type test.

The README said the runner recognises the same three layouts. It
recognises none: it hands every top-level directory to Chainsaw.

Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
Assisted-by: LLM
2026-10-28 18:53:07 +01:00
Marian Koreniuk 3d97e912b4 fix(e2e): drop the job-level runner.temp and pin the tenant kubeconfig handling
jobs.e2e.env set COZY_PROXMOX_WORKDIR from runner.temp, but the runner
context is not available in job-level env, so GitHub would refuse the
whole workflow at load time and the Proxmox lane would stop starting.
actionlint reports it as `context "runner" is not allowed here`.

Drop the variable. run-proxmox.sh already falls back to RUNNER_TEMP,
which the runner exports into every step, and the cleanup step removes
the file from there.

Pin the behaviour the previous commit added, none of which a test
covered: the fetched kubeconfig carries no group or other bits under
umask 022, apply-tenant removes a file an earlier run left, and the
suite's finally removes the file on the keep_tenant path, so diagnostics
after it prints no tenant section. Each case goes red with its line
removed.

Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
Assisted-by: LLM
2026-10-28 18:49:12 +01:00
Marian Koreniuk 4d7d336efc fix(e2e): tell a failed pool existence read from an absent pool release
The failure-path report checked the pool release with `get hr -o name`
and discarded the exit status and stderr, so a timeout, a kill, a
Forbidden or a refused connection all printed that the run ended before
the worker pool was applied. That is the conflation the guard wrapper
exists to refuse, on the path where a slow apiserver is most likely.

Read it with --ignore-not-found instead: exit 0 with empty output is an
absent release, exit 0 with a name is one to classify, and any non-zero
exit is a failed read named through _cozy_guard_read_failure.

The pool reads also skipped the report's own deadline and checked only
the phase clock, which says yes on every path that opens no phase.
Check the deadline before the existence read, like the listing and the
addons do, and name what was not read.

The stub's ABSENT marker now answers the flag the way kubectl does. New
cases pin the failed read and the deadline; the two budget cases are
re-timed for the extra clock reading.

Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
Assisted-by: LLM
2026-10-28 18:45:50 +01:00
Marian Koreniuk 28d9d352e1 test(kubernetes): name the Proxmox fixture's node pool <cluster>-<pool>
kubernetes-nodes names its release kubernetes-nodes-<CR name> and refuses
to render one that does not start with kubernetes-nodes-<spec.cluster>-
(_helpers.tpl, since fd14126be). The Proxmox fixture named its
KubernetesNodes "md0", so helm install of the pool failed, no Machine was
ever created, and the first real run of the lane on a self-hosted stand
timed out on the Machine assert after 20 minutes.

Name the pool k8s-e2e-md0 and add a fixture test that fails on any name
the chart would refuse.

Assisted-by: LLM
Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
2026-10-28 18:44:04 +01:00
Aleksei Sviridkin ae9e7c4c64 test(kubernetes): pin RetryOnFailure on the Proxmox csi release (#4969)
The KubeVirt csi and cilium releases carry a helm-unittest pin on
`strategy.name: RetryOnFailure` for both actions, and the e2e
HelmRelease guard in `hack/e2e-chainsaw/_lib/run-kubernetes.sh` names
those two tests as load-bearing. `helmreleases/csi-proxmox.yaml` sets
the same strategy for the same reason and had no pin.

This adds
`packages/apps/kubernetes/tests/csi_proxmox_install_retry_test.yaml`, a
mirror of `csi_install_retry_test.yaml` rendered on the proxmox
substrate, and updates the guard comment that pointed at the gap. No
template change.

Verified with `helm unittest .` in `packages/apps/kubernetes` (32
suites, 349 tests).

Closes #4731


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

## Summary by CodeRabbit

* **Tests**
* Expanded automated coverage for Proxmox CSI installation and upgrade
retry behavior, including the retry interval and remediation retry
limits. The checks verify that retries continue according to the
configured settings during installation and upgrades. This update does
not change user-facing functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-28 18:30:06 +03:00
Andrei Kvapil d62056ef03 fix(foundationdb)!: pick the version from a fixed list
cluster.version took any string and passed it to the operator as the
FoundationDB version, so a tenant chose the image tag rather than a
supported release line. The application now takes version (v7.4, v7.3,
v7.1) and maps it to a tag through files/versions.yaml, the same way
postgres and mariadb do.

The lines are the ones the pinned operator ships client libraries for,
which hack/update-versions.sh reads from the operator chart. The map
keeps v7.3 on 7.3.63, the previous default, so a cluster that never set
the version renders exactly as before.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-10-23 19:14:15 +05:00
Marian Koreniuk 782e0ab64d fix(e2e): validate the leak allowlist against every suite layout
lg_allowlist_validate accepted only <suite>/chainsaw-test.y*ml, while the
selectors already see step-file suites and a Test nested below the suite
directory. A suite in one of those layouts could not be allowlisted at
all: the run stopped before the first suite. Validate the allowlist with
the rule hack/select-e2e.sh enumerates suites with, spelled the same way
on both sides and cross-referenced so they move together.

Describe the three layouts once in the e2e-chainsaw README, point the
agent doc at it, and list kubernetes-proxmox as the parked suite.

Closes #4659

Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
Assisted-by: LLM
2026-10-19 23:20:19 +02:00
Marian Koreniuk 61238500dd fix(e2e): keep the Proxmox tenant kubeconfig per run and remove it
run-proxmox.sh wrote the tenant's super-admin kubeconfig to $TMPDIR with
the process umask and never removed it, so it outlived the run on the
self-hosted runner, world-readable under the usual umask. dump_diagnostics
then read whatever sat at that path, so a run that failed before the
fetch reported the previous run's tenant as its own.

The file now lives under COZY_PROXMOX_WORKDIR, which the workflow points
at runner.temp. It is written under umask 077, removed at the start of
apply-tenant so this run never sees a leftover, removed again in the
suite's finally, and once more in the workflow's credential cleanup step
for a run killed before finally runs.

Closes #4806

Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
Assisted-by: LLM
2026-10-19 23:17:50 +02:00
Marian Koreniuk cac152048e fix(e2e): read the KubernetesNodes pool release in the HelmRelease guard
cozy_guard_all_helmreleases reads the parent and the kubernetes-<test>-
addons. The worker pool is kubernetes-nodes-<test>-md0, an Application
HelmRelease the apiserver stamps with RetryOnFailure like the parent, on
the node-join path through the talos-reconcile Job it renders. Its name
does not start with the addon prefix, so a failed action on it was never
read, on the passing path or in the failure-path report.

Read it on both paths. The pool name is derived from the parent, the way
the addon prefix is, so the two cannot drift. The failure-path report
checks that the release exists first: a run that failed between the
parent and the pool apply has none, and that is a fact about where the
run stopped rather than a failed read.

Closes #4737

Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
Assisted-by: LLM
2026-10-19 23:14:09 +02:00
Marian Koreniuk 98d0402096 test(kubernetes): pin RetryOnFailure on the Proxmox csi release
The KubeVirt csi and cilium releases carry a helm-unittest pin on
strategy.name: RetryOnFailure for both actions, and the e2e HelmRelease
guard names those two tests as load-bearing. helmreleases/csi-proxmox.yaml
sets the same strategy for the same reason and had no pin. Add the mirror
test and update the guard comment that pointed at the gap.

No template change.

Closes #4731

Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
Assisted-by: LLM
2026-10-19 23:08:02 +02:00
Andrey Kolkov 9cc3a9e459 test(vmi): add VMInstance and standalone VMDisk backup/restore e2e round-trips
The disabled hack/e2e-chainsaw/backup/ suite was a Chainsaw port of the retired
backup.bats, written against the old backup API, so the VMInstance and standalone
VMDisk Velero paths had no e2e coverage. Replace it with two round-trips in the
vminstance suite that drive the validated examples/backups/{vmi,vmdisk} scripts,
so the documented demo and the automated test stay in step.

Both run un-gated in CI:
- vminstance-2-backup-roundtrip boots a tiny cirros VM (u1.nano, 2Gi), backs it
  up via BackupClass + BackupJob, restores in place, and asserts the VM returns
  Running off a disk PVC the restore created: a new uid, Velero's restore-name
  label and the -orig- PVC keepOriginalPVC leaves behind. The label alone is not
  enough, because existingResourcePolicy: update also stamps it on a PVC Velero
  only patched, which moves no data. A guest-side boot proof is unavailable: the
  KubeVirt CR disables serial-console logging and cirros ships no guest-agent.
- vmdisk-2-backup-roundtrip imports a 5Gi disk, backs it up, and restores it in
  place with the same three assertions. The controller prepares the target
  (suspend HelmRelease, delete DataVolume, rename PVC). On a WaitForFirstConsumer
  class the Velero data mover waits for a Pod to select a node for the restored
  PVC, which a detached disk never gets, so the script starts a pause Pod on the
  new PVC, standing in for the VM that would attach it.

The disk storageClass is an override (default replicated; the CI steps pass the
lane's COZY_E2E_STORAGE_CLASS, 'local' on the DRBD-off gate) and the script waits
are bounded so their worst case stays under the 40m op timeout, sized to keep the
whole parallel:1 suite inside the 215m job cap. On failure the catch also dumps
the Velero Restore, DataDownload and DataUpload objects. Each harness stands up a
throwaway in-cluster BSL (the platform cozy-default BSL derives the external S3
endpoint, unroutable in kind); its finally resumes the suspended HelmReleases,
reclaims the keepOriginalPVC orig PVC and its LINSTOR volume, and drains the
Velero backups before deleting the BSL. The example scripts are non-interactive
so the harness can drive them.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-10-13 18:05:45 +04:00
Andrey Kolkov 43b02bda30 test(e2e): run the Redis backup round-trip against Valkey
The Redis backup demo now takes APP_KIND=Valkey, which switches the app
kind and the chart prefix the helpers derive sentinel and Secret names
from. The demo BackupClass routes Valkey to the same demo strategy.

The valkey suite gains valkey-backup-roundtrip, which drives that demo
end to end: seed a marker, back it up, restore it in place and into a
copy. It writes to tenant-root's seaweedfs through seaweedfs-s3, so a
seaweedfs change now selects the valkey suite too, and since the
round-trip runs examples/backups/redis as its harness, an edit there
selects both the redis and the valkey suites. The reduced E2E install
keeps the backup stack for the valkey suite, as it does for the other
backup round-trip suites.

Signed-off-by: Andrey Kolkov <androndo@gmail.com>
Assisted-By: LLM
2026-10-13 12:34:24 +04:00
Aleksei Sviridkin cfa9c5a5c7 fix(e2e): keep chainsaw asserts polling through transient API errors (#4867)
## What this PR does

Chainsaw asserts in the e2e suites can fail long before their timeout.
Chainsaw v0.2.15 keeps polling only on NotFound and stops on any other
API error. When etcd changes its leader, kube-apiserver answers the GET
with a 500, and whichever suite happens to be polling goes red. Between
2026-09-27 and 10-11 this hit 6 of the 397 e2e attempts that reached
chainsaw, which is 6 of the 23 assert failures in that window. Each one
has `etcdserver: leader changed` in the apiserver logs within 0.2 s of
the failure. Five of the six came after 10-09, and four of them were the
computeplane ingress-nginx assert.

The fix is kyverno/chainsaw#2761, which is still open and not in any
release. Until it is released, the e2e sandbox image builds chainsaw
v0.2.15 from source with that PR's commit applied unchanged, using the
same Go minor and build flags as the v0.2.15 release binary. The build
runs the tests of the patched packages. It also runs chainsaw against a
fake apiserver that answers 500: a short run of 500s must not fail the
assert, and an endless one must fail it at its timeout. Plain v0.2.15
fails both cases after the first GET.

The Dockerfile header lists what to remove once a chainsaw release
carries the fix. The README sentence added here has to go too, and so
does the Renovate rule that keeps the builder on Go 1.26.

The upstream PR also breaks one chainsaw test outside the packages it
touches, `Test_runE` in `pkg/commands/assert`. Its expected string lacks
the new `; operation terminated with error: context deadline exceeded`
suffix. I found it with a full `go test ./...` on v0.2.15 with and
without the patch. The image build tests only the patched packages, so
that test does not run here.

One trade-off: when the API stays broken, an assert or `error` step now
runs to its timeout instead of failing within seconds, so a red run
caused by a dead API gets longer. The proxmox e2e lane runs the chainsaw
installed on its runner, not the sandbox image, so it does not get this
fix.

Known gaps, left for later: the new test covers only an assert on a
named object, not one that lists objects or an `error` operation, and
its skip guard does not notice if no test runs at all. The patch file
has no `-whitespace` entry in `.gitattributes`, so `git diff --check`
reports whitespace inside the upstream patch. A multi-platform build of
the image would run the builder tests once per platform; the image is
built for amd64 only.

Closes #4720

### Screenshots

Not a UI change.

### Downstream repositories

None of them builds or uses the e2e sandbox image; the change stays
inside this repo's CI.

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

### Release note

```release-note
fix(e2e): a chainsaw assert in the e2e suites keeps polling through a transient API error such as an etcd leader change, instead of failing the suite long before its timeout.
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* End-to-end sandbox assertions now retry after transient Kubernetes API
errors instead of failing immediately. Persistent errors still cause
assertions to fail when their timeout expires.
* **Testing**
* Added checks that verify assertions recover after temporary API errors
and fail within the expected timeout when errors persist.
* **Maintenance**
* Updated sandbox image build and dependency update settings for the
Chainsaw testing tool.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-12 17:25:39 +03:00
Yasar Rahman 9a88afe6ca feat(redis): run Sentinel on the same image as the Redis pods
The Sentinels were left on redis:6.2.6-alpine when the operator moved to
the freshworks-oss fork, so that its new compiled-in default could not
move them on upgrade. 6.2 is end of life, and every instance now runs a
Sentinel one or two major versions behind the Redis it monitors.

The rollback risk behind that pin, a newer engine rewriting dump.rdb in
a format the older one cannot load, does not apply to Sentinel. It
stores no RDB or AOF, only sentinel.conf on an emptyDir that the
config-copy init container rewrites from the operator's ConfigMap on
every start, so moving back to an older image starts from a clean
config. Taking the image from the same version map as the data pods also
means one image per node instead of two.

The e2e comment that relied on busybox's timeout exit code in the
Sentinel container is updated, since that container now carries
coreutils.

Signed-off-by: Yasar Rahman <yasar.rehman@gmail.com>
Assisted-by: LLM
2026-10-11 13:16:11 +05:00
Aleksei Sviridkin 813c618086 fix(e2e): keep chainsaw asserts polling through transient API errors
Chainsaw v0.2.15 ends an assert or error operation on the first API
error other than NotFound. kube-apiserver answers a GET with 500 when
etcd changes its leader, so a leader change that lands inside a poll
fails the step long before its own timeout, on whichever suite happens
to be polling.

The sandbox image now builds v0.2.15 from source with the fix proposed
upstream in kyverno/chainsaw#2761, applied unchanged, until a chainsaw
release carries it. The build runs the tests of the patched packages
and a test that drives chainsaw built from the patched tree against a
fake apiserver: a run of 500s must not fail an assert, and a
persistent one must fail it at its timeout rather than at the first
error.

Closes: #4720

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-11 11:04:12 +03:00
Aleksei Sviridkin be2387323b feat(gateway)!: reserve a passthrough listener's name by the spec alone (#4638)
## What this PR does

This keeps the TLS-passthrough services on port 443 of the tenant
Gateway and makes the passthrough rules hold on Cilium 1.20.2. It is
stacked on #3343 and has to land with it (stack #4717).

On 1.20, upstream Cilium marks an HTTPS-terminate listener and a
TLS-passthrough listener on one port `Conflicted` when their hostnames
overlap, and drops both from Envoy. That took `*.<apex>` and the `api`,
`vm-exportproxy` and `cdi-uploadproxy` passthrough listeners down
together on every tenant Gateway in `dns01` and `existingSecret` mode
(#3912). #3343 now ships a patched operator that conflicts the pair only
when the hostnames are equal, so the layout stays as it is on main: one
Gateway, one address, everything on 443. What remains is the
equal-hostname case and the cleanup that 1.20 allows.

Under `http01`, a name a passthrough listener answers is now reserved by
the spec alone. Before, the controller dropped the terminate listener
for such a name only once a TLSRoute was attached to the passthrough
listener and its backend Service resolved, which copied how Cilium
builds filter chains. Without a working TLSRoute, an HTTPRoute on
`api.<apex>` kept its terminate listener next to `tls-api`, and on 1.20
that equal-hostname pair takes both listeners down. Now such an
HTTPRoute gets no listener and no certificate, TLSRoute or not. If all
its hostnames are reserved it reads `Accepted=False` with
`NoMatchingListenerHostname`. If it also claims a free hostname it stays
`Accepted` and is served there. The backend lookup, its Service and
ReferenceGrant watches and their RBAC rules are gone.

Other changes:

- `tlsPassthroughListeners` is accepted under `dns01` and
`existingSecret`. The old refusal relied on 1.19.5 serving all ports
from one Envoy listener; 1.20 splits ports whose SNI overlaps.
- A TenantGateway reads `Ready=False` with reason
`GatewayClassUnsupported` when its GatewayClass lists features in
`status.supportedFeatures` but not `TLSRoute`, while it renders
passthrough listeners. A class that lists nothing is not judged. Cilium
1.20.2 lists `TLSRoute`.
- Comments, the README and one error message no longer describe Cilium
1.19.5 as the shipped release.
- The gateway chart writes `tlsPassthroughServices` on the publishing
tenant only, behind the same `gateway.enabled` switch and
`publishing.exposedServices` filter as the platform TLSRoutes. Before,
every tenant Gateway got the three passthrough listeners with nothing
routed to them, and the reservation above would have taken those names
from a child tenant's own HTTPRoutes.

The uniform `allowedRoutes.kinds` on port 443 from #3087 stays. The
Cilium bug behind it is fixed since 1.19.6, but several tests pin it, so
removing it is a separate cleanup.

### Upgrade notes

Only the publishing tenant keeps the `api`, `vm-exportproxy` and
`cdi-uploadproxy` passthrough listeners. That is `tenant-root` unless
`publishing.ingressName` says otherwise, and only for names in
`publishing.exposedServices` while `gateway.enabled` is on. Other
tenants lose these listeners on upgrade. Nothing shipped was attached to
them, so a child tenant's own HTTPRoute on `api.<child-apex>` keeps its
listener and certificate.

Two narrow cases do change. On the publishing tenant under `http01`, an
HTTPRoute on an exposed passthrough name loses its listener and
certificate when the platform TLSRoute for that name forwards nowhere
(the owning package is not installed, or its backends do not resolve). A
TLSRoute attached by hand to a child tenant's `tls-*` listener loses its
parent. To keep that listener, merge-patch `tlsPassthroughServices` into
the tenant's `TenantGateway` after the upgrade. From then on the chart
does not render the key there, and later upgrades leave the patch alone.
`dns01` and `existingSecret` do not change: the wildcard listener still
covers such a name, and the passthrough listener answers it by its exact
name.

Tested locally: Go tests for the controller, `go vet`, helm-unittest for
`extra/gateway` and `cozystack-controller`, `make generate` and `make
manifests` with no diff. The gateway e2e scenario checks that a child
tenant renders no passthrough listener. It then declares `tls-api` and a
passthrough listener on a native port on that tenant, checks that the
tenant stays Ready with no route behind either, and checks the
reservation against `tls-api`. Unit tests catch a regression in either
change. In e2e, dropping the reservation turns the gateway suite red,
and dropping the chart scoping does not, because CI never sets
`gateway.enabled` (#4846). The controller was not run on a live cluster;
the operator patch it depends on was, see #3343.

### Downstream repositories

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

### Release note

```release-note
feat(gateway)!: the tenant Gateway renders the api, vm-exportproxy and cdi-uploadproxy passthrough listeners only on the publishing tenant, for the names publishing.exposedServices lists while gateway.enabled is on. Under certMode http01 a name a TLS-passthrough listener answers no longer gets an HTTPS listener on 443, with or without a TLSRoute behind it. On the publishing tenant, an HTTPRoute on such a name whose TLSRoute forwards nowhere loses its listener and certificate. A TLSRoute attached by hand to a tls-* listener of another tenant loses its parent. An HTTPRoute with some reserved and some free hostnames used to be rejected whole; it is now Accepted and its free hostnames get a listener and a certificate. tlsPassthroughListeners is accepted under dns01 and existingSecret. A TenantGateway whose GatewayClass does not list TLSRoute in its supported features reads Ready=False while it renders passthrough listeners.
```

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

* **New Features**
* TLS passthrough listeners are supported with HTTP-01, DNS-01, and
existing-secret certificate modes. Edge-terminated Gateways remain
unsupported.
* Declaring a passthrough hostname reserves it from HTTPS termination
and certificate creation, even when no route is attached.
* Passthrough listeners are rendered only for exposed services on the
enabled publishing Gateway.
* **Bug Fixes**
* Gateways that declare passthrough listeners but explicitly lack
TLSRoute support are now reported as not ready.
* Routes targeting a hostname served by a passthrough listener on a
different Gateway are correctly withdrawn.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-10 16:59:17 +03:00
Aleksei Sviridkin ad3f15d4f3 feat(gateway)!: reserve a passthrough listener's name by the spec alone
The TenantGateway controller withheld the HTTPS-terminate listener for a
name a passthrough listener answers only once a TLSRoute was attached
there and resolved a backend, mirroring how Cilium builds its filter
chains. That kept a served endpoint up on Cilium 1.19.5, but it tied
every route's status to one implementation's translator and needed
Service and ReferenceGrant watches to land the deferred decision.

The shipped operator accepts a wildcard terminate listener beside a
narrower passthrough listener on 443 and keeps conflicting every other
overlap. A terminate listener for the passthrough name itself is not
narrower, so on 443 the rule now protects the dataplane outright; on a
native port Gateway API would serve the pair, and the rule holds as a
policy that keeps a platform name with its passthrough listener. Reading
it off the spec makes the outcome the same on every implementation.

Under http01 an HTTPRoute claiming such a name gets no listener and no
certificate whether or not a TLSRoute is attached. A route whose every
hostname is reserved reads Accepted=False with
NoMatchingListenerHostname; one that also claims a free hostname stays
Accepted and is served there, as Gateway API accepts a route whose
hostnames intersect at least one listener. dns01 and existingSecret are
unchanged: their wildcard listener still covers such a name, and the
passthrough listener answers it by its exact server name.

The chart writes the passthrough services on the publishing tenant
alone, so the names reserved by default are the ones the platform
publishes there through its own TLSRoutes. The shape an upgrade
changes is an HTTPRoute on such a name whose platform TLSRoute forwards
nowhere, because it never attached (the package owning the service is
not installed) or because none of its backends resolve: that route kept
a terminate listener and now gets none.

The backend resolution, its watches and the RBAC rules that served them
go away.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-10 06:32:42 +03:00
Aleksei Sviridkin 6144805bea fix(mariadb): mount the chart's my.cnf under a .cnf name (#4843)
## What this PR does

The MariaDB chart's my.cnf has not been read by the server since the
mariadb-operator bump to v0.28.1. Up to v0.0.27 the operator mounted the
referenced ConfigMap key as `/etc/mysql/conf.d/my.cnf` whatever the key
was called. Since v0.0.28 the file takes the key's own name, so the
chart's key `config` became `/etc/mysql/conf.d/config`. The server's
`!includedir` reads only `*.cnf` files, so it skipped it, and every
instance has been running on stock MariaDB defaults. On the default
`t1.nano` preset that means a 128M buffer pool inside a 128Mi memory
limit.

This renames the key to `my.cnf` in the ConfigMap and in the MariaDB CR.
The file content is unchanged. I started all four shipped versions
(10.6.24, 10.11.15, 11.4.9, 11.8.5) with the rendered file under a 128m
memory limit, and each one came up with the chart's values. The
MySQL-only options in the file log a warning and are otherwise ignored.
They and the deprecated `innodb_flush_method` are tracked in #4842.

Tests: a helm-unittest suite pins the key on both sides, and the mariadb
chainsaw suite gets a step that compares `@@innodb_buffer_pool_size` on
the running server with the value in the rendered my.cnf.

This changes behaviour on every existing instance, see the release note.
Each instance rolls its pods once because the projected volume in the
pod template changes: replicas first, then the primary with a
switchover. After that the server runs with every setting in the chart's
file. Instances created before the operator bump (May 2024) ran with
these settings before. Newer ones never did.

Closes #4836

### Screenshots

Not a UI change.

### Downstream repositories

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

### Release note

```release-note
fix(mariadb): the chart's my.cnf is applied again. Since mariadb-operator v0.28.1 it was mounted under a name the server ignores, so MariaDB instances ran on stock defaults. Every existing instance rolls its pods once on upgrade (replicas first, then the primary with a switchover) and then runs with the chart's settings. That changes behaviour: buffer pool, redo log, connection limits and caches are sized from the instance's resources; sql_mode becomes NO_ENGINE_SUBSTITUTION, so strict mode is off and invalid values are truncated with a warning instead of rejected; innodb_flush_log_at_trx_commit=2 can lose up to about a second of committed transactions if the node crashes; innodb_fast_shutdown=0 makes every pod shutdown slower.
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* MariaDB configuration is now stored and read using the `my.cnf` key. A
verification check confirms the running server applies the configured
buffer-pool size.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-10 04:38:28 +03:00
Aleksei Sviridkin 8ab6d4948f test(mariadb): assert the server runs with the chart's my.cnf
The chart's unit tests can pin the ConfigMap key and the CR reference,
but not what mariadb-operator does with them or which files the server
then reads from /etc/mysql/conf.d. Compare innodb_buffer_pool_size as
the live server reports it against the value in the rendered my.cnf,
which differs from the server default on this fixture's preset.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-10 02:47:37 +03:00
Aleksei Sviridkin da28ea7ee8 ci(e2e): run nightly and release-tag e2e on the container lane
Nightly and the rc gate still booted the management nodes as QEMU
guests, the substrate the merge-gating lanes left behind. On it the
tenant Kubernetes suites have missed their node-join deadline on most
nights since 2026-10-02, each failure spends the whole deadline, and
the job reaches its timeout before the alphabetically later suites
run. Over the same days those suites passed on every PR run that
selected them, where the nodes are Talos containers and a tenant
worker sits one virtualization level shallower.

Both lanes now use the PR lanes' container substrate. Nightly's e2e no
longer waits for the nocloud disk, which it does not boot; the disk is
still built and published. A manual e2e-tag dispatch against a tag
that predates the container lane fails early and names the ref to
dispatch from instead.

DRBD, the replicated StorageClass and the Cozystack Talos node image
lose their last e2e coverage with this. The docs and comments that
pointed at nightly and the rc gate as that coverage now say so.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-09 16:57:43 +03:00
Myasnikov Daniil fd0a0fa29f test(networking): bind deny probe to live pod
Require a real admission ClusterIP and carry the EndpointSlice target UID into the direct-backend coordinate. Tie that coordinate to a non-empty current Pod snapshot so stale endpoints or masked list failures cannot produce a policy verdict.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 05:00:04 +03:00
Myasnikov Daniil 79e236aa7a test(networking): harden webhook regression proof
Probe one exact ready admission EndpointSlice backend throughout the measurement window and reject missing or ambiguous Service ports. Ensure the kubeovn-webhook Helm suites run through package discovery.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 05:00:03 +03:00
Aleksei Sviridkin 4c3c838a66 test(e2e): run the computeplane pool at two workers
A single-vCPU worker has 900m to allocate after the kubelet
reservations, and the tenant addons' CPU requests use all of it. When
cert-manager's startupapicheck hook pod is the last one created it
stays Pending, the install times out, and the reinstall after
remediation runs into the same full node. Whether it is last depends
on install ordering, so the suite passed or failed on the race.

Two workers match the kubernetes suites' pool and leave room for the
hook pod.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-06 20:32:44 +03:00
Aleksei Sviridkin d35493a64d test(e2e): cover the computeplane module end to end
The module had render tests only. Nothing checked that turning it on
in a tenant yields a working cluster: the Kamaji control plane, a
worker pool shaped by the module's allowlist, an admin kubeconfig the
tenant can neither list nor read, the pinned cert-manager and
ingress-nginx addons running inside the cluster, and a teardown that
removes the in-cluster addons before the control plane and leaves
nothing behind.

The suite runs the module in its own child tenant so the shared
tenant-test is never toggled. It supplies its own pool because the
default md0 asks for the replicated StorageClass, which the container
lane does not have. On failure it prints the worker image source,
since the pool allowlist drops talos and a ComputePlane worker cannot
use the in-sandbox image cache.

The computeplane source depends on the kubernetes application and
runs its own ingress-nginx, so an ingress-nginx change now selects
this suite too. It also owns apps/kubernetes-nodes, which used to
reach no suite and run everything; kubernetes-nodes-application now
maps to both kubernetes suites and computeplane, so a pool chart
change keeps the suites that create KubernetesNodes natively.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
Assisted-by: LLM
2026-10-06 00:05:42 +03:00
Aleksei Sviridkin 415430bcc7 fix(e2e): fail on a failed release of cilium and csi as on the parent, and report addon remediation (#3594)
## What this PR does

The history guard in the tenant Kubernetes e2e suite checked only the
parent `kubernetes-<test>` HelmRelease. A failed action on one of the
addon releases it installs left nothing the suite looked at, so a run
showed only the downstream symptom, usually the node-join deadline, and
never named the release behind it.

The guard now reads every addon release of the parent. It selects them
from the cluster by name prefix, so a release added to the chart later
is covered too. The rule:

- A `failed` or `uninstalled` Snapshot fails the run on any release
whose install strategy is `RetryOnFailure`. That is the parent (the
aggregated apiserver stamps it on every Application HelmRelease) and the
tenant `cilium` and `csi`.
- The same footprint on the other addons, which run the default strategy
with `retries: -1`, is printed as a note and the run stays green. For
the CRD addons that configuration is tracked in #3555.

A transient `cilium` or `csi` failure that later recovers on a retry
still turns the run red, by design, as long as its Snapshot still reads
`failed`. One recovery leaves no such Snapshot: when an install fails
and the very next attempt succeeds, Helm marks the failed install
`superseded`, because there is no deployed release to supersede, and the
history reads like an ordinary upgrade. The guard cannot see that case
from the statuses alone. The code and the docs now say so, a test pins
it, and detection is left to a follow-up.

An earlier revision of this PR tried to tell a teardown apart from a
retried failure and made only `uninstalled` fatal. That split does not
hold. In helm-controller v1.5.0, the version the platform installs, a
release with an active retry strategy never remediates.
`atomic_release.go` returns an upgrade for a failed release before it
reaches the remediation switch, and the two other paths that uninstall
(a changed release target, and deletion) both clear history afterwards.
So on these releases `uninstalled` never shows up, and `failed` is the
only signal. The parent keeps the rule it already had on main, and
`cilium` and `csi` now follow it too.

The failure paths get the same reading. The guard runs after the
assertions, while the deadlines a failed addon breaks end the script
earlier, so the same classification is also printed, without a verdict,
from the node-join collector and from the exit handler. That handler is
now armed at the tenant CR apply, so the management-cluster waits before
the tenant answers are covered too. Smaller changes that came with this:

- Every guard read is bounded, and a failed read is never folded into
"no history".
- The parent must read back a non-empty strategy. An empty value there
means the jsonpath stopped resolving, and without this check every
footprint would quietly turn into a note.
- The history read uses `HELMRELEASE_HISTORY_JSONPATH`, and the
upstream-type pin in `internal/fluxcontract` now accepts the bounded
wrapper the read goes through.

`docs/agents/e2e-testing.md` §6 and checklist item 8 describe the new
rule.

On related issues: this adds the `local` that #3774 reports missing in
`helmrelease_has_remediation_cycle`, but not the sweep over the other
`_lib/` helpers that the issue also asks for. For #3773 it rewrites the
guard comment the issue quotes. The waits without a backstop are not
touched.

Closes #3553.

### Screenshots

Not a UI change.

### Downstream repositories

I walked the trigger map against the diff. It touches
`hack/e2e-chainsaw/_lib/`, `hack/*.bats`, one Go test under
`internal/fluxcontract`, two spend-order comments in the kubernetes
suites, and `docs/agents/e2e-testing.md`. No package, schema, CRD, make
target or user-facing document changes.

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

### Release note

```release-note
fix(e2e): the HelmRelease history guard now also covers the tenant addon releases. A failed or uninstalled Snapshot fails the run on the parent, cilium and csi, which retry in place; other addons get a note. The same classification is printed on the failure paths where the guard itself cannot run.
```
2026-10-05 17:12:38 +03:00
IvanHunters 2188f16461 test(vminstance): cover VMDisk clones from an image and from a disk (#4586)
## What this PR does

Closes #3482.

The `vminstance` suite now covers both VMDisk clone paths, which had no
e2e test at all. One disk is cloned from a golden image with
`source.image`, and a second disk is cloned from the first with
`source.disk`. For each clone the test checks that the DataVolume points
at the expected source PVC and becomes `Ready`, that its PVC is `Bound`,
and that the VMDisk reports `Ready` and `WorkloadsReady`, the same way
the standalone VMDisk test does. The `Bound` check is separate because
CDI derives `Ready` from the DataVolume phase alone, and can reach
`Succeeded` from the claim's annotations before the claim binds; a
`phase: Succeeded` assert would only repeat `Ready`.

The golden image is a fixture the suite creates itself.
`vm-default-images` is an opt-in package and the e2e install does not
enable it, so `alpine-3.21` from the default collection is not there to
clone. The suite imports the same Alpine 3.21 image into `cozy-public`
under the name `e2e-alpine-3.21`, so it can never collide with an image
the package owns. This is the only download in the new test, and the
`source.disk` case reuses the first clone as its base disk.

The long wait sits on the DataVolume, not on the HelmRelease. Since
#4380 a vm-disk release goes Ready once the DataVolume exists and the
clone's progress is reported through the VMDisk `WorkloadsReady`
condition, so the test does not assert the HelmRelease at all. Chainsaw
deletes the clones before the golden image at cleanup.

The clone itself is only proven by this PR's E2E run, on the `local`
class of the PR lane.

### Screenshots

Not a UI change.

### Downstream repositories

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

The diff touches only `hack/e2e-chainsaw/vminstance/`, and nothing in
the trigger map covers it.

### Release note

```release-note
test(vminstance): e2e now covers VMDisk clones from a golden image (source.image) and from another disk (source.disk).
```
2026-10-05 15:14:38 +03:00
IvanHunters 4b1a75a332 test(vminstance): dry-run evict the VM launcher pod (#4585)
## What this PR does

Part of #4422.

The e2e suite now asks KubeVirt whether it would let a VM launcher pod
be evicted, without evicting it. Until now nothing in e2e evicted a
launcher pod, so the drain behaviour set up in #4403 was known only from
reading KubeVirt. Once the `vminstance` VM is running, the suite sends a
dry-run Eviction (`dryRun=All`) for its virt-launcher pod and checks the
answer. Nothing gets deleted.

A dry run still goes through the KubeVirt eviction webhook, which is
registered with `sideEffects: NoneOnDryRun` in v1.9.0. With
`LiveMigrateIfPossible` its answer follows the VMI's `LiveMigratable`
condition. That condition depends on storage, so the test reads it
instead of assuming it:

- `LiveMigratable=True`: the eviction must be refused with the webhook's
`Eviction triggered evacuation of VMI "<ns>/<name>"` message (HTTP 429).
A drain retries on that while the VM migrates away.
- `LiveMigratable=False`: the eviction must be allowed. The old
`LiveMigrate` value refused this case with "is not live-migratable".
KubeVirt v1.9.0 creates no PodDisruptionBudget for a VMI, so no PDB can
refuse it either.
- Anything else, including a missing condition, fails the test.

The allowed answer alone proves little, because the webhook also lets an
eviction through under `None`. So the test also asserts
`evictionStrategy: LiveMigrateIfPossible` in the VMI spec, where
virt-api copies the cluster value at creation. After the dry run it
checks that the launcher pod is the same object and not terminating, and
that the VMI has no `evacuationNodeName`.

On the PR lane only the `False` branch runs. Post-install prep pins
`StorageProfile/local` to RWO because DRBD is off there, and an RWO disk
makes the VMI non-migratable. Nightly and tag runs use `replicated`,
where I expect the `True` branch, but I have not seen this suite run on
that lane yet. A real drain and a workload update after a KubeVirt bump
are still open in #4422.

I ran the script against a stub `kubectl`. Both expected answers pass,
and it fails on every wrong answer I tried, such as a refusal for a
non-migratable VMI, a PDB-style 429, a missing condition, two launcher
pods, or a replaced pod after the dry run. `chainsaw lint` (v0.2.15) and
shellcheck pass.

### Screenshots

Not a UI change.

### Downstream repositories

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

The diff touches only `hack/e2e-chainsaw/vminstance/` and the suite
README, and nothing in the trigger map covers either.

### Release note

```release-note
test(vminstance): the e2e suite now dry-run evicts the VM launcher pod and checks the KubeVirt eviction decision against the VMI's LiveMigratable condition.
```


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

* **Tests**
* Added end-to-end coverage for dry-run eviction of a running virtual
machine’s launcher pod. The test checks the VM’s live-migration policy
and `LiveMigratable` condition, expecting eviction to be refused when
the VM is live-migratable and accepted otherwise.
* Verified that the dry run leaves the launcher pod unchanged and does
not record an evacuation node on the VM.
* **Documentation**
* Documented the test’s behavior and the local `jq` requirement for
running it.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-05 15:14:32 +03:00
Aleksei Sviridkin 33cf6972bb test(qdrant): exercise the tenant trust anchor end to end (#4701)
## What this PR does

The qdrant e2e suite now runs with TLS on and checks the tenant trust
anchor end to end. Before this, the fixture left `tls` unset with
`external: false`, so the chart rendered no CA and no
`TenantProjection`, and nothing in e2e reached the controller or
`tenantsecrets`. Setting `tls.enabled: true` turns TLS on without the
LoadBalancer that `external: true` would allocate.

The first new step checks that the anchor is published. The
`qdrant-test-ca` sentinel has to report `Ready=True/Projected`, and
`qdrant-test.tenant-ca` has to carry both the `tenant-ca` and
`tenantresource=true` labels. The projection must hold only `ca.crt`
with the same fingerprint as the chart's CA, and `tenantsecrets` must
serve it. The key-bearing `qdrant-test-ca` and `qdrant-test-tls` must
answer NotFound there, and any other error fails the step.

The second step checks that the anchor is what decides trust. It reads
the anchor through `tenantsecrets`, port-forwards the qdrant Service and
runs `openssl s_client` with that file as the only CA, hostname check
included. A CA with the same subject and another key must fail, and so
must the runner's default trust store. A handshake failure counts only
when the output shows a verification error, so a refused connection does
not pass as a rejection. Plaintext HTTP must get no response. Curl exits
7 and 28 fail the step, and a second verified handshake afterwards
proves the forward was still alive.

TLS does not break the existing steps, because none of them talk to
qdrant. `create-qdrant`, `wait-helmreleases-ready`,
`verify-statefulset-and-storage` and `verify-pvc-reclaim-on-delete` only
read Kubernetes objects: no `http://`, no port 6333 or 6334, no curl or
exec, and the suite has no backup round trip. The release now also
renders two Issuers, two Certificates and the sentinel. The chart
switches its probes to HTTPS with `enable_tls`, so `readyReplicas: 1`
now covers a pod serving TLS.

Local checks: `chainsaw lint test` (v0.2.15) says the file is valid.
`helm template` with the fixture's spec renders 2 Certificates, 2
Issuers and 1 TenantProjection, and none without TLS. I ran the
handshake logic against a local `openssl s_server` with a CA and leaf
shaped like the chart's. The anchor passes, the wrong CA and the default
store fail on verification, a wrong hostname fails, plaintext gets
`000`, and a closed port is not counted as a rejection.

A sentinel with a missing source (`SourceNotFound`) is not tested here,
the reconciler unit tests cover it.

Review notes I left as they are:

- Through `kubectl port-forward` the local socket is always accepted, so
curl exits 7 and 28 only catch a dead forward process. A false pass
would need qdrant to answer plaintext and a pod-side blip on that one
request at the same time.
- qdrant may reset the socket on the plaintext request, and some kubectl
versions drop the whole port-forward on a pod-side error. Then the
handshake after it turns the step red, not green. If CI shows `lost
connection to pod` there, the plaintext check should get its own
port-forward.
- The suite's `catch` has no `describe` of the `TenantProjection`. The
sentinel assert prints its conditions when it fails, but a red Secret
assert or script after it would not.
- The vendored chart's `servicemonitor.yaml` hardcodes `scheme: http`,
so metrics scraping fails for any qdrant with TLS on. That was already
the case before this PR, and the suite does not check metrics. Tracked
in #4703.

cert-manager does not set owner references here, so `qdrant-test-ca` and
`qdrant-test-tls` stay in the namespace with their private keys after
the app is deleted. That is tracked in #4704. The nats half of #3706 is
tracked in #4702.

Closes #3706

### Screenshots

No UI change.

### Downstream repositories

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

The diff touches only `hack/e2e-chainsaw/qdrant/`, which no entry in the
trigger map covers.

### Release note

```release-note
test(qdrant): e2e now runs qdrant with TLS on and checks that a client trusting only the tenant-published CA reaches it, while a wrong or missing anchor and plaintext HTTP are refused.
```
2026-10-05 14:14:36 +03:00
IvanHunters 9196b9798e fix(e2e): bound the dataplane capture collector by wall clock (#4640)
## What this PR does

The e2e data-plane collector limited itself by counting pods and
LoadBalancers, but both of its callers stop it by the clock.
`hack/cozytest.sh` gives it 600s and the Chainsaw catch gives it 300s.
One affected pod could already run past the smaller limit, and the
LoadBalancer capture, which runs last, was the part that got lost. I
moved the collector to a wall-clock budget and fixed the other issues
filed against it.

- Every kubectl call now goes through `dp_run`. It cuts the call's
timeout to what is left of `COZY_DATAPLANE_BUDGET` and starts nothing
once the budget is spent. The default is 270s, and `cozytest.sh` passes
570s. The pod section stops 90s early, so the LoadBalancer section
always gets time to start. A call the deadline cut says so, a call cut
by its own bound names that bound, and a refused call says it was not
started and names its section; exec blocks carry that line inside the
block. The node capture and the LoadBalancer probe used to blame an
unanswered lookup or a missing cni-server when the budget stopped a
call. Now they name the budget (#3671).
- A static test fails on any kubectl call that bypasses `dp_run` and
lists the lines. It matches by command position, so a call inside a
double-quoted `$(...)` is still checked (#3694).
- Per-pod and per-LoadBalancer capture files join namespace and name
with `_`, which neither can contain. A second run into the same
directory appends, with the run separator `capture-notes.txt` already
uses (#3679).
- `capture_node` and `capture_lb_node` no longer write their callers'
loop variables. A static test fails when a function assigns a name that
a `read -r` loop or another scope owns (#3675).
- The tests that shrank every timeout to one second now cut only the
read under test. A `timeout` shim decides by argv, so a slow machine
can't cut a read the test needs answered (#3685).
- `cilium-dbg bpf lb list` runs unfiltered in every affected node's
capture, not only for a failing LoadBalancer (#3689).

The call-site comment in `.chainsaw.yaml` and the collector paragraph in
`docs/agents/e2e-testing.md` now match the code. Every behaviour change
has a test in `hack/capture-dataplane.bats` that failed on the code
before it.

Two parts of the issues live elsewhere. The runner stopping a bats file
at its first failing test is #4620, and a conntrack read filtered by
service VIP is #4623.

From #4629 I fixed the items on lines this PR already changes: the node
capture now gets the run separator, a failed speaker log read writes a
note, the endpoint tcpdump says when it falls back to the geneve
interface, and the per-read timeout knobs are validated (a zero there
would otherwise look like a spent budget). The rest of #4629 stays open:
`ovn-lflows.txt` and `lb-speaker-logs.txt` are still replaced on each
run, and two comments still describe older behaviour.

- Closes #3694
- Closes #3671
- Closes #3679
- Closes #3675
- Closes #3685
- Closes #3689
- Refs #4629

### Screenshots

Not a UI change.

### Downstream repositories

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

### Release note

```release-note
fix(e2e): bound the failure-path data-plane collector by a wall-clock budget, keep its per-pod and per-LoadBalancer captures from overwriting each other, and capture the cilium service map for ClusterIP failures.
```


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

* **Improvements**
* Data-plane diagnostics now follow a configurable time budget,
reserving time for LoadBalancer captures and stopping operations when
the budget is spent.
* Capture reports distinguish failed, successful, unknown, and
budget-limited checks. LoadBalancers with successful probes are skipped
for heavy capture.
* Repeated captures are preserved together, and limits help control
LoadBalancer diagnostic collection.
* Diagnostics include additional network details from relevant nodes
during probe replay.
* **Documentation**
* Updated guidance covers capture time limits and how unanswered
EndpointSlice reads are reported.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-05 14:10:25 +03:00
IvanHunters f209c477e2 test(e2e): read the opensearch release history through the pinned name (#4689)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->

## What this PR does

The opensearch suite now reads HelmRelease history through the shared
jsonpath, and a unit test fails if any Chainsaw suite goes back to a
copied literal. Before this, the suite carried its own copy of the
expression, and the upstream contract test in `internal/fluxcontract`
only checks `HELMRELEASE_HISTORY_JSONPATH`. I checked the copy against
helm-controller `api` v1.5.1, the version go.mod pins: the history field
is `history` and each snapshot has `status`. So the read works today, it
was just not pinned.

The suite passes `-o "jsonpath=${HELMRELEASE_HISTORY_JSONPATH}"` now,
which gives kubectl exactly the same argument as before. The new test
reads every suite under `hack/e2e-chainsaw/`. A suite that calls
`helmrelease_has_remediation_cycle` must fill the local it hands the
guard from the shared name, and no suite may assign that name itself. It
reuses the matchers that already pin `run-kubernetes.sh`. The test is
red on main and green with the fix.

One known limit. The second-assignment check counts over the whole file,
which fits `run-kubernetes.sh` because it runs as one shell. A Chainsaw
suite runs each `script` step in its own shell, so an unrelated
`statuses=` in another opensearch step would turn the test red with
nothing broken. That fails toward red, never toward a silent pass, so I
left it.

Closes #4550

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

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

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
test(e2e): the opensearch suite reads HelmRelease history through the shared jsonpath, which is now pinned against the upstream Flux type for every Chainsaw suite that runs the remediation guard.
```


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

* **Tests**
* Expanded validation across Chainsaw end-to-end suites to check
consistent HelmRelease history handling in remediation checks, including
the order and isolation of script steps.
* The OpenSearch suite now uses the shared history lookup. Empty-history
checks and remediation-cycle failure handling are unchanged, and there
is no change to end-user behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-05 14:08:51 +03:00
IvanHunters b4fbaf00a2 test(e2e): keep gateway and redis-tls op ceilings above their own waits (#4643)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->

## What this PR does

Two Chainsaw script ops had a timeout smaller than the waits inside
them. When the op ceiling fires first, Chainsaw kills the shell: the
script never prints its own error, and a cleanup trap never runs.

The gateway listener-set step could poll for 660s under a 6m op. Its
cleanup trap also deleted two tenants with no limit on the wait. Each
delete now runs under an outer `timeout`, because kubectl's own
`--timeout` does not cover discovery or a stalled request. A tenant
delete gets 300s, the same 5m that `.chainsaw.yaml` gives a delete op.
The parent is deleted only after the child is gone, because deleting a
parent while its child is still uninstalling wedges the parent's
cleanup. A failed tenant delete fails the step with kubectl's own error,
so a leaked tenant shows up here and not in a later suite. The op is now
24m, which covers the polls plus those bounds.

The redis-tls CA-projection step has 30s read caps that add up to well
over its 5m op. Raising the op would not hold, because several reads run
in loops. So all reads now share a 240s step budget. No read starts
after the budget runs out, and a read that is still running gets cut off
when it does. A read that hits its bound, or finds the budget already
spent, says which read it was.

Two exec checks in the same step blamed the 15s redis-cli cap for any
failure, including a refused exec or a missing container. They now print
the exit status and what the exec printed.

The step also runs under bash now, for pipefail. The sandbox's `/bin/sh`
is dash, which rejects `set -o pipefail`. Chainsaw 0.2.15, the version
the sandbox image installs, supports a `shell:` field on script ops.
Without pipefail, a failed Secret read took base64's zero exit status
and was reported as a wrong source name.

The private-key check read the projection a second time, so a failed
second read made it pass. It now checks the value the step already
fetched.

I added `hack/chainsaw-op-budgets.bats` to guard all of this. It holds
the gateway op above its polls plus its delete bounds, keeps the
child-before-parent order, holds the redis budget under its op with
every kubectl call going through the clamped helper, and runs the redis
step's script against a stubbed kubectl to check each failure message
and the clamp. On main, 11 of its 12 tests fail. The one that passes is
the healthy-stub control.

Closes #4114

Closes #4250

Closes #4246

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

Not a UI change.

### Downstream repositories

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

The diff touches two Chainsaw suites and adds one unit test file under
`hack/`. It moves or renames nothing a downstream repository reads.

### Release note

```release-note
test(e2e): the gateway listener-set and redis-tls projection steps now fail on their own errors instead of being killed by the op timeout, and redis-tls reports the real cause of a failed read or exec.
```


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

* **Tests**
* Added automated checks to verify gateway operations have sufficient
time to complete and cleanup deletions are bounded and correctly
ordered.
* Expanded gateway scenarios to cover cleanup failures: if child-tenant
deletion fails or times out, the parent is left in place; deletion
failures are reported.
* Updated Redis TLS checks with bounded read times and a shared
execution limit. Failure reports include command output and status, and
checks cover private-key material in the decoded projection.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-05 14:07:37 +03:00
IvanHunters 1ca603a7b2 test(e2e): wait for the S3 gateway to accept a key before using it (#4694)
## What this PR does

The bucket e2e suite fails now and then because the S3 gateway rejects a
key that COSI already reports as granted. The error is `The access key
ID you provided does not exist in our records.` from `mc alias set`. In
job 109130131391 the BucketAccess assert finished at 21:23:43.57 and the
first `mc alias set` failed at 21:23:44.82. In job 109396174094 cap1 and
cap2 were accepted, and cap3 was rejected two seconds into the step.

Granted access is not the same as a usable key. In seaweedfs-cosi-driver
v0.3.1, `DriverGrantBucketAccess` writes the new key into
`/etc/iam/identity.json` on the filer and returns. The S3 gateway in
SeaweedFS 4.45 reloads that file later, from a filer metadata
subscription (`weed/s3api/auth_credentials_subscribe.go`). In our chart
the driver writes through `seaweedfs-filer` and the two gateways follow
`seaweedfs-filer-client`, so the change may have to pass through the
other filer replica first.

The suite now waits until the gateway accepts the key before it writes.
Every alias in both write steps goes through `await_key`, which uses the
same port-forward as the writes and so asks the same gateway pod. It
waits only while the answer is the InvalidAccessKeyId text, for at most
60s. Any other error fails at once, and so does an attempt cut off at
its 15s cap. When the window runs out, the step fails with a message
that names the BucketAccess.

One probe costs at most 77s, so both steps go from a 5m to an 8m op
timeout. The inner budgets now sum to 409s and 441s, and the budget
comments say so.

I checked the probe locally against a fake `mc`:

```text
case=ok rc=0 attempts=1 elapsed=0s
  Added `a` successfully.
case=lag rc=0 attempts=3 elapsed=5s
  Added `a` successfully.
case=other rc=1 attempts=1 elapsed=0s
  FAIL: mc alias set for BucketAccess bucket-test-admin failed (exit 1) with an error other than the key not being loaded yet
case=never rc=1 attempts=31 elapsed=60s
  FAIL: S3 gateway still rejects the key of BucketAccess bucket-test-admin after 60s of probing (the gateway never loaded it, or the driver never wrote it)
```

A hanging `mc` fails on its first attempt after 15s with exit 124.
`chainsaw lint test` from v0.2.15 accepts the file.

The read-only upload check gets fixed along the way. Before, an unloaded
`ro-user` key also made the upload fail, and that failure passed as the
expected denial.

One thing I left as it is: `await_key` is copied into both steps,
because chainsaw steps share no shell, and moving it to `_lib/` would
run every suite on each edit.

Closes #4580

### Screenshots

Not applicable, no UI change.

### Downstream repositories

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

### Release note

```release-note
test(e2e): the bucket suite waits for the S3 gateway to accept a new key before writing with it, instead of trusting BucketAccess readiness.
```
2026-10-05 14:07:16 +03:00
IvanHunters 858ca4d0a3 test(e2e): cover monitoring OIDC modes with a Chainsaw suite (#4678)
## What this PR does

Monitoring OIDC gets live-cluster e2e coverage again. It had none since
the two pre-Chainsaw BATS suites were removed without a port. This adds
a Chainsaw suite in `hack/e2e-chainsaw/monitoring/` that ports what they
asserted, plus the in-place switch from an inline config to a
`secretRef` that nothing covered.

The suite takes one Monitoring release in `tenant-test` through three
modes in a row: System, CustomConfig with an inline config, and
CustomConfig with a `secretRef`. It creates that release itself and
never reads the tenant-root monitoring instance, so it keeps working if
#4023 or #4479 turns root monitoring off.

What each phase checks:

- System mode: cozystack-api admits `spec.oidc` and both HelmReleases go
Ready. The Grafana CR points `auth.generic_oauth` at the cozy realm, has
no `role_attribute_path`, and has `skip_org_role_sync` and
`oauth_allow_insecure_email_lookup` forced. The KeycloakClient is
confidential and references the generated Secret, which holds 32
alphanumeric characters. The audience scope is bound to this release's
clientId. There is no chart-owned `KeycloakRealmGroup`. The users-Job
carries the users and the mode, and `grafana-deployment` reads the
client secret from the Secret.
- Switch to inline: the Grafana CR takes the tenant's map, keeps the
forced keys and drops every System-only key. The users-Job reruns with
the new list. The KeycloakClient, the audience scope and the client
Secret are pruned, and the Deployment stops referencing that Secret.
- Switch to `secretRef`: the inline `auth.generic_oauth` block is gone
from the Grafana CR. The operator Secret is mounted at
`/etc/grafana/oidc` on both the CR and the Deployment. This checks the
mount only and does not prove Grafana loads the file, see #4679.
- After the install and after each switch, neither release may show a
remediation cycle. That means no `failed` or `uninstalled` snapshot, and
the latest revision must be exactly 1, then 2, then 3. The revision
check is there because helm-controller truncates history after the
previous deployed snapshot. An upgrade that failed, rolled back and then
passed on retry leaves no failed entry, only extra revisions.

I did not assert `GF_PATHS_CUSTOM_INI`. I could not find anything in
Grafana that reads it (#4679), and pinning it would make possibly dead
wiring part of the contract.

The coverage split with the render tests is this.
`packages/system/monitoring/tests/oidc_test.yaml` keeps the
fresh-install render of each mode, including the CustomConfig negatives
(no Keycloak objects, no client Secret). This suite covers what a render
cannot see: admission, the Deployment the Grafana operator builds, and
what a switch between modes prunes or leaves behind. The Keycloak
objects are checked as written, so creating the client in Keycloak is
not asserted. Removing it is, because the operator's finalizer holds the
KeycloakClient until the client is gone from Keycloak.

Before the first apply the suite also waits until the tenant's
`cozystack-values` carries `oidc-enabled: "true"`. The outer release
copies `_cluster` into the inner release once, so a Monitoring applied
right after the tenant is created can render from stale values and fail
the System-mode guard for the full 15m. That is what the first CI run on
this PR hit.

The Keycloak CRDs are required, not probed for. The e2e install always
enables platform OIDC, so a missing CRD is a broken install. The removed
suites skipped in that case, and their probe could not tell an absent
API group from a present one.

The selector picks the suite up by name from
`cozystack.monitoring-application` and `cozystack.monitoring`. A change
to `system/monitoring`, `extra/monitoring`, `monitoring-rd` or
`grafana-operator` now runs only this suite instead of the full run. The
tenant-root Grafana bring-up stays covered by the install BATS while it
exists. One selector unit test relied on `cozystack.monitoring` reaching
no suite, so it now renames that source in its copy of the graph.

The second commit keeps Keycloak changes on every suite that drives a
Keycloak client. Before this PR a change to `system/keycloak` or
`system/keycloak-operator` escalated to the full run. With the
monitoring suite it would select monitoring alone, because
`monitoring-application` is the only source that depends on
`keycloak-operator`, and `kubernetes-latest` with its OIDC lifecycle
would drop out. `src_to_suites()` now maps both to `kubernetes-latest
monitoring`. The mapping has to be explicit because
`kubernetes-application` has no `dependsOn` on `keycloak-operator`
(#4680). A `postgres-operator` change now also reaches
`kubernetes-latest`, through Keycloak's database. The selector comment
and the e2e doc no longer use `system/postgres-operator` as the example
of a path with a source that reaches no suite.

Cost: one monitoring stack bring-up, two in-place upgrades and teardown.
I expect 10 to 20 minutes on every full run. The selector change makes
the CI run on this PR a full run, so the suite's own timing is in the
chainsaw report. A red run can spend up to 16m on each of four waits,
since each one sits a minute above the 15m timeout of the release it
waits on.

The suite cannot run locally, so I checked it this way:

- `chainsaw lint test` at v0.2.15, the sandbox version, passes.
- I ran the suite's own steps against a local envtest apiserver.
Chainsaw applies the real manifests, and a stand-in for helm and the
operators renders the chart from the stored Monitoring spec. With the
chart intact all nine steps pass. Each of 30 single breakages of the
chart or of the simulated controllers turns the expected step red.
- `hack/select-e2e_test.bats` and `hack/select-install_test.bats` pass.
The new Keycloak selection case fails without the mapping and when
`kubernetes-latest` is dropped from it.

A browser login through Keycloak is out of scope, same as in the removed
suites.

Left for later:

- KeycloakClient and scope `status` after the operator creates them in
Keycloak is not checked.
- `hack/e2e-chainsaw/README.md` lists suites by hand. I added
`monitoring`, but `backup`, `kafka-metadata` and `kubernetes-proxmox`
were already missing.
- The `operator diff selects all dependent app suites` selector test
could also check `monitoring`.
- The comment on the Keycloak mapping says both suites wait for the
finalizer to remove the client. The monitoring suite does wait.
`kubernetes-latest` only reads once after its upgrade.
- The ready checks compare `observedGeneration` with
`metadata.generation` through `contains()`. Both are fields of the same
object, not a number literal, so the v0.2.15 literal gotcha should not
apply, but no live run has passed this suite yet.
- The inline fixture carries no `enabled: "true"`, so that phase checks
a disabled OAuth block. The asserts do not depend on it.

Closes #4051

Closes #3785

### Screenshots

Not a UI change.

### Downstream repositories

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

### Release note

```release-note
NONE
```


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

## Summary by CodeRabbit

* **New Features**
* Added end-to-end coverage for Monitoring installations using system
OIDC, inline custom configuration, and Secret-based configuration. The
tests verify readiness, rendered resources, configuration cleanup, and
upgrade history.
* Updated test selection so Keycloak-related changes also run the
Monitoring suite.
* **Documentation**
* Added Monitoring to the documented Chainsaw suite list and clarified
how changed paths determine test coverage.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-05 14:07:10 +03:00
Aleksei Sviridkin 2df578bffd test(e2e): read the opensearch release history through the pinned name
The opensearch suite wrote the history jsonpath out as a literal, so
the contract test that runs HELMRELEASE_HISTORY_JSONPATH against the
upstream Flux type never saw the expression this suite hands kubectl.
A field rename upstream would have reached it only as an empty read.

The suite now takes the shared expression, and the contract test reads
every Chainsaw suite that calls the guard, so a suite copying the
literal, or assigning the shared name after sourcing the library, fails
in the unit lane instead of going unchecked. The read is matched inside
the script step that calls the guard, because every step runs in a
shell of its own and a read in another step never reaches the call.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-03 11:39:34 +03:00
Aleksei Sviridkin 8cb1023788 fix(e2e): fail on a failed release of cilium and csi as on the parent
The history guard covered only the parent Kubernetes HelmRelease. A
failed action on one of the addon releases it installs left no mark
the suite inspected, so a run surfaced only the downstream symptom,
typically the node-join deadline, with nothing naming the release
behind it. The tenant CNI and CSI are the sharp cases, since they
carry a cluster-wide precondition.

The guard now reads every addon release of the parent, selected from
the cluster by name prefix so a release added later is covered too.
A "failed" or "uninstalled" Snapshot fails the run on a release whose
install strategy is RetryOnFailure: the parent, which the aggregated
apiserver stamps that way, and cilium and csi. The same footprint on
the other addons, which pair the default strategy with retries: -1,
is printed as a note and leaves the run green.

The footprint is not split into a teardown and a retried failure.
In helm-controller v1.5.0, the version the platform installs, and
unchanged in v1.5.1, a release with an active retry strategy never
remediates: atomic_release.go returns an upgrade for a failed
release before it reaches the remediation switch (lines 465-475), and
the two other paths that uninstall, a changed release target and
deletion, both call ClearHistory afterwards. On a RetryOnFailure
release an "uninstalled" Snapshot is therefore unreachable, and the
"failed" one is the only signal. A retry that later recovered still
fails the run, by design, with one blind spot the statuses cannot
close: a failed install that the very next attempt recovers is
marked superseded by Helm, because there is no deployed release to
supersede instead, and its history reads like an ordinary upgrade.

The guard runs after the assertions, while every deadline a failed
addon breaks ends the script earlier, so the same classification is
also printed without a verdict from the node-join collector and from
the exit handler. That handler is now armed at the tenant CR apply,
so the management-cluster waits before the tenant answers are
covered too. Every read is bounded and never folded into "no
history", and the parent must read back RetryOnFailure: an empty
value there means the jsonpath stopped resolving, and any other value
means the apiserver stamp is gone, either of which would turn its
footprint into a note.

The history read takes its jsonpath from HELMRELEASE_HISTORY_JSONPATH,
and the upstream-type pin accepts the bounded wrapper the read goes
through.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-03 06:59:31 +03:00
Aleksei Sviridkin cc731e3105 test(e2e): keep redis-tls failures legible under the op ceiling
The CA-projection step caps each kubectl at 30s, but more capped reads
run than its 5m op can cover, so a degraded apiserver could spend the
ceiling and leave nothing but "signal: killed". The reads now share a
step budget held under the op, and a read that hits its bound or finds
the budget spent says which read it was.

Two exec checks reported the 15s redis-cli cap for any failure, sending
a reader after a slow redis when the exec was refused or the container
was missing. They now report the exit status and what the exec
printed. The step also runs under bash with pipefail: under dash a
failed Secret read took base64's zero status and surfaced as a wrong
source name. The private-key check reads the projection already
fetched, since a second read that failed made it pass.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-03 01:42:53 +03:00
Aleksei Sviridkin a057d738d1 test(e2e): give the gateway listener-set op room for its own waits
The step's polls can wait 660s under a 6m op, and its cleanup trap
deletes two tenants with no bound on the wait. When the op ceiling
fires first, Chainsaw kills the shell: the poll that was running never
reports, the trap never runs, and both tenants stay behind for every
later suite.

The deletes keep waiting, child before parent, since a parent deleted
while its child is still uninstalling wedges its own cleanup. Each
delete runs under an outer timeout with a kill grace, because kubectl's
--timeout bounds only the wait after the request and not discovery or
a stalled request. The parent is deleted only once the child is gone.
A failed tenant delete fails the step with kubectl's error, its exit
status and the tenant left behind, rather than passing with the leak.
The op is sized above the polls plus those bounds, and a unit guard
holds it to that sum and the cleanup to that order.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-03 01:42:53 +03:00
Aleksei Sviridkin 9873b39a2a test(vminstance): cover VMDisk clones from an image and from a disk
Only source.http had e2e coverage, so a regression in either clone
path would ship unnoticed. The suite now clones a VMDisk from a golden
image in cozy-public and then clones that disk again, checking that
each DataVolume points at the expected PVC and becomes Ready, that its
PVC is Bound, and that each VMDisk reports WorkloadsReady. CDI derives
Ready from the DataVolume phase alone and can reach Succeeded from the
claim's annotations before the claim binds, so Ready does not stand in
for the binding.

vm-default-images is opt-in and the e2e cluster does not enable it, so
the suite creates its own small golden image under an e2e name rather
than relying on the package. The second clone reuses the first disk,
so the Alpine image is the only download.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-02 22:40:24 +03:00
Aleksei Sviridkin 9809b22280 fix(vm-disk): report DataVolume readiness through WorkloadsReady (#4380)
## What this PR does

Nothing on a vm-disk application tells whether CDI ever finished
populating the disk. Its WorkloadMonitor has no pods to count, and
`Ready` follows the Helm release. So an import or clone that never
completes can leave a green application over a disk nobody can use.
cozystack/cozystack#2908 is one way to get there: a clone into a
StorageClass that cannot place a replica where the source data sits. The
expensive part there was the silence, not the stuck clone.

The disk state was in the DataVolume phase all along, and nothing read
it. With this change the WorkloadMonitor in cozystack-controller reads
it. The monitor lists the DataVolumes its selector matches and reports
not operational while any of them is in flight, Failed, Unknown or has
no phase. An upload counts as in flight until it completes. That
surfaces on the application as `WorkloadsReady=False` with reason
`DataVolumeNotReady` and a message that names the DataVolume and its
phase, for example `DataVolume vm-disk-x is ImportInProgress`.

`Ready` still follows the HelmRelease, as for any other kind
(`convertHelmReleaseToApplication` in
`pkg/registry/apps/application/rest.go`), and for a vm-disk it no longer
waits on the DataVolume at all. So the disk state has to be visible
outside `Ready`, and this PR puts it where operators look by default:

- `kubectl get` on any application kind gets a `WORKLOADS` column next
to `READY`. It shows the `WorkloadsReady` status, or `<none>` when the
application has no WorkloadMonitor.
- The console badge turns amber and reads `DataVolumeNotReady` while
`Ready` is True and `WorkloadsReady` is not True for that reason. A
failing `Ready` still wins. The console shows only the disk case: other
kinds need a separate decision, because a stopped VM legitimately has no
pod and reports `WorkloadsReady=False` too.

A Helm readiness gate on the DataVolume would be the obvious
alternative, and it is not used on purpose. helm-controller runs that
wait inside its reconcile, so an importing disk would hold one of the
shard's worker slots for the whole install window, and a disk stuck for
good would keep taking the slot back. cozystack/cozystack#4145 has the
details. The monitor costs no slot, keeps watching after install, and
covers existing disks without anyone writing to them.

Without any gate, the install would still wait on the DataVolume.
helm-controller's default poller falls back to a resource's `Ready`
condition for kinds it has no rule for (`checkReadyCondition` in
fluxcd/cli-utils `pkg/kstatus/status/status.go`), so a vm-disk install
could hold a worker slot for the whole install timeout. The
ApplicationDefinition therefore gets a `healthCheckExprs` entry for
DataVolume with `current: "true"`, the same idiom postgres-rd uses for
its ScaledObject. With it Helm never waits on the DataVolume. The entry
is pinned by a helm-unittest in `packages/system/vm-disk-rd`, which gets
a `test` target for it.

Release settings reach a HelmRelease only when the application is
created or updated, so an existing disk gets the entry on its next
update. The reconciler that syncs definitions onto existing releases
copies chartRef, valuesFrom and labels only. That affects any kind with
release settings, so it is filed as cozystack/cozystack#4409 and not
fixed here. `WorkloadsReady` covers every disk right away, since the
monitor needs no write to the application.

CDI copies the DataVolume labels onto the PVC it creates and makes the
DataVolume its controller, so the monitor also publishes a `pvc-<name>`
Workload for the disk. That Workload reads not operational while its
DataVolume is not ready, even though the PVC is already Bound, so
`kubectl get workloads` agrees with `WorkloadsReady`. While the
DataVolumes cannot be read it keeps the verdict it has, like the
monitor.

The WorkloadMonitor CRD now requires at least one selector label. An
empty selector would match every Pod, PVC, Service and DataVolume in the
namespace. No chart renders one.

When an application has several monitors that are not operational and
only some of them name a cause, `WorkloadsReady` carries the named
messages and reason only. A vm-disk has one monitor, so it does not hit
this.

### Which phases count as ready

Settled phases pass: `Succeeded`, `PendingPopulation`,
`WaitForFirstConsumer`, `Paused`. A disk with `bindImmediately: false`
on a `WaitForFirstConsumer` class rests in `PendingPopulation` or
`WaitForFirstConsumer` until a VM consumes it. A multi-stage import
rests in `Paused` until its next checkpoint, and the vm-disk chart
renders no checkpoint source. These report ready while holding no data,
because that is where such a disk is supposed to sit.

`UploadReady` is not one of them. CDI enters it when the upload server
pod is running and ready, and leaves it for `Succeeded` only when that
pod succeeds (`updateStatusPhase` in
`pkg/controller/datavolume/upload-controller.go` at v1.66.1), so the one
phase covers a disk nobody has uploaded to and a half-uploaded one. A
`source: upload` disk therefore reports `WorkloadsReady=False` from
creation until `virtctl image-upload` completes.

A phase the controller does not know also reads as ready, so a CDI
release that adds or renames a phase cannot turn working disks red. The
cost goes the other way: a renamed in-flight phase silently reads as
settled. The list wants a re-read whenever the CDI version moves. It
matches the named phases of the CDI v1.66.1 API, the version vendored in
`packages/system/kubevirt-cdi-operator`.

### Clusters without CDI

CDI comes with the IaaS bundle, while cozystack-controller runs
everywhere, and CDI can be installed while the controller is running. A
watch on a kind whose CRD is absent never syncs, and then the whole
controller does not start. So the watch is not set up at startup. A
runnable adds the DataVolume watch once the kind is served. It waits for
the first reconcile, because only from then on does a new watch start at
once, and then checks discovery right away and every 30s after that. It
installs the DataVolume reader only after the DataVolume informer has
synced within a timeout, because a reconcile context has no deadline and
a List on an informer that never syncs would block every WorkloadMonitor
reconcile. The watch source is started once: a controller-runtime `Kind`
source adds an event handler to the informer and never removes it, so on
a timeout the next check waits on the source it already has instead of
starting another one. RBAC needs no change: the controller's ClusterRole
already has read on everything.

When the DataVolumes cannot be read, because the reader is not installed
yet or the List fails, the monitor still publishes its replica counts
and keeps its last DataVolume verdict, and a List error is returned so
the request is retried and recorded as a `DataVolumesUnavailable`
Warning event on the monitor. The status of a monitor without
DataVolumes is not affected by a failing List. A disk whose verdict is
already recorded does not read as ready in the window after a controller
restart. A monitor with no recorded verdict yet, which is every monitor
right after the upgrade that brings this change and any disk created
right after a restart, reads ready until the DataVolume watch has
synced. Because the watch starts on the first reconcile rather than on
the next discovery tick, that window is the time to sync the DataVolume
informer plus at most one 5s requeue, not a 30s poll interval. The
window is not zero, since a monitor reconciled in it is published before
its DataVolumes can be read. From then on, a reconcile that runs before
the watch has synced is requeued after a few seconds, so the DataVolume
events that queue it are not lost. A pass that changes nothing in the
monitor status does not write it, so that requeue sends no status writes
to the API server. Once the reader is installed, every monitor that
holds a DataVolume verdict is queued once more. So a verdict whose
DataVolume was deleted while the controller was down is recomputed then,
not at the periodic resync.

### Tests

The table test in `workloadmonitor_controller_test.go` covers the named
phases of CDI v1.66.1, plus Failed, Unknown, an empty phase and a
missing status. A second table checks the message and reason for an
importing, a failed, a phaseless and a populated disk, and a third test
lists two stuck DataVolumes in both orders and expects one message. It
also covers a DataVolume outside the selector, the kind not being
served, a List error that still publishes the replica counts, and a
missing reader that keeps the last verdict. The watch tests check that
the watch starts on the first reconcile and not before it. A source that
never syncs leaves no reader and is retried on the same source, with the
reconciles in between still requeued. A List failure records the Warning
event, the sync window records none, and an unchanged pass writes no
status. A manager that stops during the wait gets the cancellation back,
and a reconcile that runs while the watch syncs is requeued without an
error. A table test covers the PVC Workload: importing, populated, a PVC
with no DataVolume controller or a controller of another kind or group,
and the unread cases. A helm-unittest in
`packages/system/cozystack-controller` pins the selector's
`minProperties`. `rest_table_test.go` pins the `WORKLOADS` column,
`rest_workloads_test.go` the condition message and reason, and
`status.test.ts` the console badge, including a halted VM that stays
green.

In `hack/e2e-chainsaw/vminstance` the existing VMDisk scenarios now wait
for the DataVolume to be Ready, then for `Ready` and `WorkloadsReady` on
the VMDisk itself. That replaces the wait on the HelmRelease. Those
asserts would pass even if the monitor ignored DataVolumes, so a new
scenario imports from an `.invalid` URL. It first waits for the importer
to have failed, which CDI reports on the DataVolume as `Running=False`
with reason `Error`, and then for `Ready=True` with
`WorkloadsReady=False` and reason `DataVolumeNotReady` on the VMDisk.
Without the first wait the VMDisk assert passed in about a second, on a
DataVolume that had no phase yet. The e2e runs on a fresh install only,
so nothing here exercises an existing release across an upgrade.

### What it does not do

#2908 lists possible fix directions, and none of them is here. The
cross-class clone still never completes, it just stops being invisible.
Keep the issue open.

While the DataVolume watch syncs, every monitor is still requeued. Only
the ones whose selector reaches a DataVolume need it, but the controller
cannot tell which those are before it can read DataVolumes.

Closes #4449

### Screenshots

There is no screenshot: I had no running console to take one from. The
badge change is pinned by the vitest cases in
`packages/system/dashboard/images/console/apps/console/src/lib/status.test.ts`:
a VMDisk whose `WorkloadsReady` has reason `DataVolumeNotReady` goes
amber, and a halted VMInstance with the generic reason stays green.

### Downstream repositories

I walked the trigger map against the diff.
cozystack/terraform-provider-cozystack is affected: its `wait_for_ready`
reads only `Ready`, so on a `cozystack_vmdisk` it returns before the
disk is populated once this lands. That needs a decision on the provider
side, so it is an issue there. The release-time bot regenerates
`packages/apps/vm-disk/README.md` into the website's virtualization
pages, and `vm-disk` is in the `VMS` list that generator reads, so the
website needs no follow-up. The ApplicationDefinition keeps its kind,
plural, prefix and path.

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

### Release note

```release-note
fix(vm-disk): report whether CDI finished populating a disk in the WorkloadsReady condition, which names the stuck DataVolume and its phase. kubectl get shows WorkloadsReady in a new WORKLOADS column for every application kind, and the console badge turns amber while a disk is not populated, so a disk whose import or clone never completes no longer looks healthy. Ready of a new or updated vm-disk no longer waits for the DataVolume, so wait on WorkloadsReady to know the disk is populated.
```
2026-10-02 19:47:02 +03:00
Aleksei Sviridkin 472f962eff test(e2e): cover monitoring OIDC modes with a Chainsaw suite
Monitoring OIDC lost its live-cluster coverage when the two pre-Chainsaw
BATS suites were removed without a port. Render tests pin each mode on a
fresh install, but not admission by cozystack-api, the Deployment the
Grafana operator builds from the CR, or what an in-place mode change leaves
behind on a running release.

The suite drives one tenant-test Monitoring release through System,
CustomConfig with an inline config, and CustomConfig with a secretRef,
so a single stack bring-up covers all three and the upgrades between
them: the cozy-realm client, scope and client Secret are pruned when
System mode is left, and the inline generic_oauth block is gone once the
secretRef mount arrives.

The monitoring package sources now map to this suite, so a change to
system/monitoring, extra/monitoring, monitoring-rd or grafana-operator
selects it alone instead of escalating to the full run, and the selector
test that relied on cozystack.monitoring reaching no suite seeds a
source that does not.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-02 15:03:00 +03:00
Aleksei Sviridkin 966fccfb37 test(vminstance): dry-run evict the VM launcher pod
No suite evicts a virt-launcher pod, so the platform's eviction
behaviour is known only from reading KubeVirt. A server-side dry-run
eviction goes through the same KubeVirt admission webhook as a drain
and returns its decision without deleting anything.

The test reads the VMI's LiveMigratable condition rather than assuming
it, because the answer depends on the storage class: under
LiveMigrateIfPossible a migratable VMI is refused with the evacuation
message, and a non-migratable one is let through. It also pins the
evictionStrategy that virt-api stamped into the VMI spec, since the
webhook passes an eviction under strategy None as well, and checks that
the dry run left the launcher pod and the VMI untouched.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-02 15:01:18 +03:00
Aleksei Sviridkin 9c9a8c225d test(seaweedfs): lower the volume size limit on a live instance and test the chart hand-off (#4707)
## What this PR does

This adds an e2e case that lowers the SeaweedFS volume size limit on a
live instance and checks that the bucket still takes writes. It also
adds a unit test for the hand-off between the SeaweedFS app chart and
the data-plane chart.

The new `seaweedfs` suite creates its own instance in `tenant-test`,
directly through the API, so the limit can change without racing the
tenant module. It installs at 200 MB and writes into one bucket until
every volume of the bucket sits between 20 and 200 MB. Then it patches
the resource to 20 and waits for both releases and the master rollout.
After that a write to the same bucket has to land, and the bucket has to
have a new volume. If the master never picks up the new limit, the write
goes to an old volume and the second check fails. 200 and 20 stand in
for the shipped 1000 and 400, because filling a bucket into that band at
real sizes would cost gigabytes in the sandbox.

The suite also checks the live master StatefulSet. The limit from the
resource has to be in the master command before and after the change,
and all four `WEED_MASTER_VOLUME_GROWTH_COPY_*` counts have to stay. The
same hand-off is checked on every PR by
`hack/seaweedfs-chart-seam.bats`. It renders the app chart, takes
`spec.values` from the `seaweedfs-system` HelmRelease and renders the
data-plane chart with them. The expected default comes from the app
chart's values, so this test does not pin 400 a second time. I checked
in a scratch copy that it goes red when the app chart's
`volumeSizeLimitMB` key or its `master:` block is renamed.

A new suite directory broke two cases in `hack/select-e2e_test.bats`, so
`seaweedfs-application` in `hack/select-e2e.sh` now maps to the new
suite too. That is one line plus its comment.

About the suite order question in #4302: this suite runs on its own
instance, so it takes nothing from tenant-root's capacity and does not
move the margin of `bucket-1-capacity`.

Left for later:

- A generic helper for the two-chart shape (render a chart, take
`spec.values` from a HelmRelease, render the target). This PR covers
only the SeaweedFS pair.
- The `status.history` remediation guard on the upgrade the suite
triggers. A failed upgrade that is retried and then succeeds would pass
here without notice.

The suite has not run on a cluster yet. Locally `chainsaw lint` passes.
I also ran the StatefulSet and HelmRelease asserts with `chainsaw
assert` against rendered objects. They pass on the expected state and
fail on a wrong limit, a missing `COPY_2`, a rollout in progress and a
stale generation, both on the StatefulSet and on the HelmRelease.

Closes #4302

Closes #4285

### Screenshots

Not a UI change.

### Downstream repositories

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

### Release note

```release-note
test(e2e): add a seaweedfs suite that lowers the volume size limit on a live instance and checks that the bucket still takes writes, and a unit test for the hand-off between the SeaweedFS app and data-plane charts.
```


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

## Summary by CodeRabbit

- **New Features**
- Added end-to-end coverage for SeaweedFS bucket resizing, including
verifying that writes succeed after the volume-size limit changes.
- Added SeaweedFS to the available and automatically selected end-to-end
test suites.

- **Documentation**
- Updated local test prerequisites to note the required tools for
SeaweedFS tests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-02 14:08:12 +03:00
Aleksei Sviridkin d1aea3f40e test(e2e): assert vm-disk readiness through WorkloadsReady
The VMDisk scenarios waited on the HelmRelease, which does not cover
the import behind it. They now wait for the DataVolume to be Ready and
then for Ready and WorkloadsReady on the VMDisk itself. The DataVolume
wait keeps the combined budget the two old waits had.

That alone would pass with the monitor ignoring DataVolumes, since an
empty monitor reports operational. A new scenario imports from an
unresolvable URL and waits for Ready True with WorkloadsReady False,
which only a monitor that reads the DataVolume phase can produce. A
DataVolume that has not started is not ready either, so the scenario
first waits for the importer to fail, which CDI reports as Running
False with reason Error, and only then asserts the VMDisk.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-02 14:02:32 +03:00
Aleksei Sviridkin a7ac37be65 test(e2e): let HR-Ready waits outlast the install timeout of the release they wait on (#4471)
## What this PR does

Stacked on #4468, which holds the raised backup waits to a start margin;
review that one first. This PR's own diff is the six commits on top of
it.

Most HR-Ready waits in the E2E suites gave up before the release they
were waiting on could fail its install. cozystack-api gives every
release it generates a 600s install timeout, and the Chainsaw asserts in
front of those releases sat at 5 or 6 minutes. When an install genuinely
failed, the suite reported a bare assert timeout against a release that
was still installing, and the reason the install failed, which the
release records when its own timeout fires, was never reached. Red runs
failed just as often as they should; they only said less.

I raised every HR-Ready wait above the install timeout of its own
release, by the 60s start margin #4468 introduces:

- Chainsaw asserts on releases cozystack-api generates go to 11m, across
bucket, clickhouse, etcd, external-dns, foundationdb, harbor, mongodb,
openbao, opensearch, postgres, qdrant, rabbitmq, redis, valkey and
vminstance.
- Asserts on nested `<name>-system` releases take their floor from the
template that renders them: 11m for external-dns, openbao and qdrant
(10m `spec.timeout`), 16m for `harbor-test-system` (15m). `harbor-test`
sat exactly on its 600s floor, which expires first because the assert's
clock starts before the install's; it goes to 11m too.
- The kuberture Package release takes its 10m from cozystack-operator
and goes to 11m.
- In the backup walkthroughs, the bucket waits go from 300s to 660s in
clickhouse, kafka, mariadb, mongodb, postgres, rabbitmq and redis, and
the two postgres waits go from 360s to 660s. The kafka bucket wait
passed no budget and silently took the helper's 300s default, which is
why earlier scans for literal budgets missed it. Each Chainsaw op that
runs a walkthrough grows by exactly what its waits gained (clickhouse
78m, kafka 46m, mariadb 48m, mongodb 42m, postgres 66m, rabbitmq 48m,
redis 36m), so a failure at any later wait keeps the headroom it had and
is still reported by the script instead of being cut off by the op.

Two waits stay as they are, on purpose:

- kafka: `kafka-rd` sets a 7h install timeout, which no budget inside
the 215-minute E2E job can clear. The kafka assert and the Kafka waits
in the kafka and kafka-metadata walkthroughs keep their numbers, and the
assert now says why in-line.
- `vm-instance-test`: `vm-instance-rd` disables the install wait, so its
release goes Ready as soon as helm has applied and there is no floor to
clear. Earlier lists of this problem included it by mistake.

The cost falls on red runs only: a suite whose release never goes Ready
now spends up to six more minutes before it reports, and in exchange the
report carries the release's own reason. Green runs are unchanged.
Suites run one after another in a single job capped at 215 minutes, so a
systemic failure that reddens many suites now reaches that cap sooner,
and a job cancelled at the cap loses its log and `cozyreport.tgz`. I
took that trade because a failed install that says why is worth more
than a red run that ends a few minutes earlier.

`docs/agents/e2e-testing.md` stopped being true in several places once
the waits were raised: it described most of the tree as under the floor,
mariadb as the only suite raised, the walkthrough bucket waits as under
it, and harbor's asserts as sitting on and under theirs. I rewrote those
sentences, and the one that pointed at "every wait above the band" of
5-6m, kept the start-margin wording from #4468, and added that a
walkthrough wait passing no budget takes the helper's default. The
comments in `hack/mariadb-first-boot-waits.bats` that quoted the old op
and bucket figures are restated; its existing checks and constants are
unchanged, because every op grew by exactly its bucket delta and each
remainder still sits at exactly its constant. I also added postgres to
the table that file holds the walkthroughs to, so its raised waits and
its op cannot drift back apart unnoticed, and a test that holds each
table suite's bucket wait to the bucket release's floor, read from the
api-server default with `bucket-rd` checked for an override; the op
check alone could not catch a bucket wait dropping back, since a lower
wait only widens the slack. redis stays out: its walkthrough waits on no
redis HelmRelease, only on the bucket and on the sentinel master, so the
table's floor check has nothing to read there.

How I checked it, since E2E cannot run locally:

- I re-ran the enumeration from the sizing section of
`docs/agents/e2e-testing.md` over every
`hack/e2e-chainsaw/*/chainsaw-test.yaml` and every `wait_hr_ready` call
under `examples/backups/`, literal or default, and held each against its
own release's floor. Every `Ready=True` assert and every `wait_hr_ready`
call a suite drives now clears its floor, except the kafka ones named
above. The opt-in etcd round-trip uses bare `kubectl wait hr` calls at
300s instead of that helper; they are listed in #4470. The `Ready=False`
assert on the rejected redis upgrade is not a readiness wait and is left
out.
- `make bats-unit-mariadb-first-boot-waits` passes, 31 of 31. For the
new postgres row, putting the op back to 50m fails only the postgres
slack test, and a postgres wait at 659s fails only the postgres floor
test. Setting any one table suite's bucket wait to 659s fails only the
bucket test, naming only that suite, and an install-timeout override on
`bucket-rd` fails it too. I ran the mutations under plain `bats` so a
fail-fast runner could not hide a second failure.
- `hack/select-e2e.sh` over this diff selects bucket, clickhouse, etcd,
external-dns, foundationdb, harbor, kafka, kuberture, mariadb, mongodb,
openbao, opensearch, postgres, qdrant, rabbitmq, redis, valkey and
vminstance.

Out of scope, and tracked in #4470: the tenant addon waits in
`hack/e2e-chainsaw/_lib/run-kubernetes.sh` (an edit there escalates the
whole E2E run), the redis and kafka-metadata `wait_hr_ready` helpers
that print no reason on timeout, and the opt-in etcd round-trip's bare
`kubectl wait` calls.

Known and left for later, none of them changed here:

- Only the mariadb Chainsaw asserts and the walkthroughs in the guard's
table are held by a test. The other raised asserts, and the redis and
kafka bucket waits, rest on literals with in-line reasons; #4470 covers
extending the guard.
- The Kafka waits in `examples/backups/kafka/04-create-kafka.sh`,
`07-restore-to-copy.sh` and `kafka-metadata/run-all.sh` stay at 300s
against the 7h floor without an in-line reason at the call site. The
doc's rule asks that of Chainsaw asserts only, and the kafka Chainsaw
assert carries it.

The postgres waits are the ones #3748 is about. Their budget now clears
the install-timeout floor and a test holds it there; what a CNPG first
boot costs on this platform is still unmeasured, so that issue stays
open.

Closes #3771

Closes #3753

### Screenshots

Not a UI change.

### Downstream repositories

I walked the trigger map in `docs/agents/contributing.md` against the
diff. It touches E2E suites, backup walkthrough scripts, a unit-lane
bats file and an agent doc, none of which any downstream repository
restates.

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

### Release note

```release-note
fix(tests): HelmRelease readiness waits in the E2E suites and backup walkthroughs now outlast the install timeout of the release they wait on, so a failed install reports its own reason instead of a bare timeout.
```


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

## Summary by CodeRabbit

* **Tests**
* Extended readiness and backup round-trip test timeouts across multiple
services to accommodate slower startup and provisioning.
* Added checks to verify that bucket readiness waits meet configured
installation-timeout requirements.

* **Documentation**
* Updated testing guidance to clarify how readiness waits relate to
release-specific timeouts and overall test budgets.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-02 12:15:58 +03:00
Aleksei Sviridkin 42faf54932 test(e2e): drop readiness asserts that HelmRelease Ready already covers (#4652)
## What this PR does

Removes e2e asserts that wait for something an earlier assert in the
same test already proves. First step of
[#2653](https://github.com/cozystack/cozystack/issues/2653), not the
whole issue.

qdrant and external-dns render the workload in a nested `-system`
HelmRelease, and both suites already assert that HelmRelease Ready. The
`readyReplicas: 1` on the StatefulSet and the Deployments goes, the
instance-label pin stays because the catch collectors select on it. I
checked on a dev cluster that a parent HelmRelease waits for its nested
one (a Qdrant with a broken `storageClass` kept the parent in
`Progressing` until its install timeout, then it failed naming the
nested HelmRelease), details in [this
comment](https://github.com/cozystack/cozystack/issues/2653#issuecomment-5929644802).

vminstance: both PVC `Bound` asserts follow a DataVolume `Ready` assert,
and a DataVolume is not Ready before its PVC is Bound.

Left alone on purpose: the qdrant PVC assert (the reclaim-on-delete step
only proves anything if the PVC was seen first, same in kafka), openbao
sts and PVC (they run after unseal), operator-created workloads, and the
Service and Endpoints shape checks. Chainsaw isn't installed on my
machine, so the suites are checked by CI only.

### Screenshots

No UI changes.

### Downstream repositories

The diff only edits `hack/e2e-chainsaw/*/chainsaw-test.yaml`. The
trigger map's `hack/` entry is for cozystack/ccp, whose skills anchor on
`hack/package.mk`, `hack/common-envs.mk` and `make generate`, none of
which change here. No box ticked.

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

### Release note

```release-note
test(e2e): the qdrant, external-dns and vminstance suites no longer repeat readiness checks that the HelmRelease Ready assert already covers.
```


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

## Summary by CodeRabbit

* **Tests**
* Updated end-to-end checks to use Helm release readiness for Deployment
and StatefulSet scenarios, rather than separately asserting ready
replica counts.
* Removed the requirement for PersistentVolumeClaims to reach `Bound` in
the VMDisk and VMInstance scenarios; DataVolume readiness checks remain.
* Retained instance-label checks and clarified that PVC reclaim
validation requires the claim to be observed first.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-02 09:50:19 +03:00