Commit Graph
65 Commits
Author SHA1 Message Date
IvanHunters 318be935ca fix(e2e): capture Talos logs for tenant worker join failures (#3548)
## What this PR does

Addresses #3513.

When tenant Kubernetes workers miss the 18-minute Ready deadline, the
E2E runner now mints a one-hour `os:reader` Talos client certificate
from the tenant's existing issuer and runs authenticated `talosctl` from
a hardened same-namespace diagnostics Pod. It captures bounded `dmesg`
and kubelet output for every worker VMI in the cozyreport snapshot,
records missing IPs and command exit codes, continues across unreachable
workers, and preserves the original test failure.

The diagnostics Pod has no service-account token or Secret volume;
credentials are streamed into an `emptyDir`, and the Certificate,
Secret, and Pod are labeled for cleanup. Workers that have not reached
Talos `apid` can only yield a bounded connection error, while workers
that reached `apid` provide guest logs. This adds the evidence needed to
distinguish the remaining guest boot failure but does not claim to
resolve its underlying cause. The readiness budget and success path are
unchanged.

Because this changes a shared E2E helper, test-impact analysis selects
the full E2E suite for this PR.

### Screenshots

Not applicable; no UI changes.

### Testing

- `hack/cozytest.sh hack/run-kubernetes-talos-diagnostics_test.bats` (10
tests)
- `hack/cozytest.sh hack/run-kubernetes-drain_test.bats`
- `hack/cozytest.sh hack/cozyreport-talos.bats`
- `bash -n hack/e2e-chainsaw/_lib/run-kubernetes.sh`
- `git diff --check origin/main...HEAD`

The full `make bats-unit-tests` run reached the unrelated
`hack/migration-seaweedfs-db-adopt.bats` suite and could not continue
because Docker is unavailable in the development environment. A live
failure-path run was not performed because the available development
environment has no Talos tenant fixture; this PR is intended to exercise
that path in Cozystack CI.

### 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:

### Release note

```release-note
fix(e2e): capture authenticated Talos kernel and kubelet diagnostics when tenant Kubernetes workers miss the Ready deadline
```



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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved troubleshooting when worker nodes fail to join by collecting
per-node operating system and kubelet diagnostics.
* Diagnostics now continue across partial failures and clean up
temporary resources afterward.
* Extended Kubernetes test operation timeouts to allow failure
diagnostics to complete reliably.

* **Tests**
* Added regression coverage for diagnostic collection, timeout handling,
multi-worker scenarios, missing node addresses, and cleanup.

* **Chores**
  * Ensured the required diagnostic image is preloaded on nodes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-07 10:40:03 +03:00
Aleksei Sviridkin 0dae3ca95a fix(e2e): budget tenant scheduling separately from workload readiness (#3579)
## What this PR does

The tenant backend Deployment in the kubernetes suites had one 300s
budget to reach `condition=Available`, and two unrelated variable costs
shared it.

The first is scheduling. What the suite establishes before that point is
two tenant nodes Ready, which is weaker than schedulable: a Ready node
still carries `node.cilium.io/agent-not-ready` until the tenant cilium
agent claims it, and a node the bringup has not finished with is
`SchedulingDisabled`. In the run recorded in #3577 scheduling took 2m18s
and 1m57s in the two suites, which left the image pull to finish inside
what remained. It did not, and both suites reported the same `timed out
waiting for the condition` for what were two different shortfalls.

So this waits for a node that actually accepts a Pod before creating the
workload, on its own budget and its own failure message. The gate
encodes the scheduler's rule for a Pod that tolerates nothing (Ready,
not unschedulable, no `NoSchedule` or `NoExecute` taint) over a
`custom-columns` probe, prints the node table on both outcomes so a
timeout names the taint that held it, and treats a failed probe as
not-schedulable so an API blip cannot release it. On the happy path it
adds no wall time: it spends the seconds the Pod would otherwise spend
Pending, plus at most one 5s poll interval. On a failing run the two
budgets stack, so a run that exhausts both now gives up at ~600s where
it used to give up at 300s, inside the 40m Chainsaw script op the suites
document as a ~25m bringup. The readiness wait keeps its 300s, now
starting from a schedulable node, and dumps deployment, pod and event
state when it runs out.

The workload image is also pinned by digest. The tenant workers reach no
Docker Hub mirror (`hack/e2e-talos-image-cache.yaml` serves the Talos
worker OS disk image over HTTP and is not a registry mirror), so nginx
is pulled from Docker Hub on every run either way. The digest does not
take the pull off the critical path; it fixes what that pull returns
instead of leaving a floating tag free to change size and content under
a fixed deadline. Preloading the image would need infrastructure this
tree does not have, and is not attempted here. Nothing will bump the
pin: no renovate manager reads `hack/`, and the comment says so, because
for a throwaway test workload the freeze is the point.

What this does not claim: it removes two measured consumers from a fixed
budget, both taken from the events of a single run. Whether that budget
was the only thing making those suites red is not established from one
run, so this is not offered as the fix for a red pipeline.

`hack/run-kubernetes-schedulable_test.bats` covers the new logic: every
branch of the predicate, the multi-node scan, the poll-again path, the
deadline, and a probe that fails. Each test was verified by mutating the
helper and checking that the intended test, and no other, went red.

Note for whoever reviews alongside #3575: that branch mirrors `ghcr.io`
for tenant worker pulls, not `docker.io`, so it does not change the
Docker Hub pull described here. `git merge-tree` reports no conflict
between the two, nor with #3548.

Observed in #3577.

### 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:

The trigger map was walked against the diff. The change is confined to
`hack/e2e-chainsaw/_lib/run-kubernetes.sh` and one new `hack/*.bats`
unit test. It moves and renames nothing under `hack/`, changes no make
target, and does not touch `hack/e2e-prepare-cluster.bats`, any package,
any CRD or any namespace name, so none of the listed repositories are
reached.

### Release note

```release-note
NONE
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved Kubernetes deployment readiness by waiting for a suitable,
schedulable node before timing backend startup.
* Added clearer diagnostics when nodes are unavailable or backend
readiness fails.
* Replaced the floating backend container image tag with a fixed,
verified version.

* **Tests**
* Added coverage for node readiness, cordoning, taints, polling,
timeouts, probe failures, and deployment sequencing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-06 23:46:50 +03:00
Aleksei Sviridkin 2527e915bd fix(e2e): collect diagnostics for unready pods and record every truncated capture (#3567)
## What this PR does

The e2e diagnostic tarball exists so a red run can be explained from the
uploaded artifact alone. Several of its collectors answered "nothing is
wrong" instead — some because they selected on a column that never
carries the value they filtered for, others because they stopped early
and left nothing saying they had. Both failure modes look identical to a
healthy cluster from the outside, which is the worst possible shape for
a diagnostic.

Four selectors in `hack/cozyreport.sh` and `hack/cozyreport-summary.sh`
were reading the wrong thing:

- Per-pod evidence was chosen by the STATUS column, so a pod left at
`0/1 Running` by a readiness probe that never passes was skipped. That
is a common way for a suite to time out, and the artifact carried no
trace of the pod the run died on. Selection now compares the two halves
of the READY column; terminal `Succeeded`/`Completed` pods stay
excluded.
- The pending-LoadBalancer section filtered `$4`, which is CLUSTER-IP.
CLUSTER-IP holds an address, `None` or `<none>` and never `<pending>`,
so that loop had never executed. `<pending>` only ever appears in
EXTERNAL-IP, `$5`.
- The cozystack-apps section gated all three of its kinds on `kubectl
get crd <kind>.apps.cozystack.io`. ApplicationDefinition is a CRD but in
a different group, and Tenant comes from an aggregated APIService with
no CRD to ask about, so the probe failed and the section never ran.
- The kamaji section was gated on `cozy-linstor`'s `linstor-controller`
deployment rather than kamaji's own, so a cluster without LINSTOR got no
kamaji logs and no TenantControlPlanes — on exactly the runs that need
them.

The summary had three of its own: the HelmRelease listing printed `$5`
where STATUS is the Ready condition's whole message, reducing every
failed release to one word like "Helm"; the Flux sources listing used a
jsonpath with a nested filter that kubectl rejects outright, so it
printed nothing on every report ever taken; and pod AGE was read as
`$6`, which shifts whenever RESTARTS prints the `7 (4m12s ago)` form —
that is, for exactly the rows worth reading.

The second half is bounding. The pod, VM, VMI and services walks now run
under a count cap and a wall-clock budget, and every read is
individually time-boxed. The point is not the bounds themselves but that
each one names itself when it fires: a walk stopped by the cap or the
clock leaves `COLLECTION-TRUNCATED.txt`, a pod list that never returned
leaves `COLLECTION-FAILED.txt` instead of the empty tree a healthy
cluster produces, a log cut short says so on its own last line, and a
`--tail` bound is recorded beside the logs rather than inside them,
since the platform's controllers log JSON per line and a trailing prose
line costs a parser the whole file. A knob value the collector could not
use is reported too, so a truncation note quoting `600` is never
mistaken for the `10m` somebody actually set. Where `timeout` is absent
the reads run unbounded rather than not at all, and the section says
which of the two happened, because a missing local binary is not a
statement about the cluster.

One behaviour change worth stating plainly, because it is a trade and
not a pure gain: the per-pod section used to be uncapped and read
container logs whole, and it is now bounded by `COZY_REPORT_PODS_MAX`
(40), `COZY_REPORT_PODS_BUDGET` (600s) and `COZY_REPORT_POD_TAIL`
(2000). Below those thresholds the new tree is a strict superset of the
old one — the old selector missed `Running 0/1` entirely and skipped
logs for `Pending` pods. Above them the artifact is smaller than before,
every drop is named in the tree, and `COZY_REPORT_POD_TAIL=-1` with a
larger `COZY_REPORT_PODS_MAX` restores the old reach. Because widening
the selection and capping the walk in one change could otherwise let a
newly-in-scope pod evict one the uncapped walk always collected, the
ordering splits by evidence class before namespace: pods that are not
`Running` — the class the collector took before this — all rank ahead of
pods that are `Running` but unready, so the cap can only ever drop a pod
the old collector did not keep either.

`hack/e2e-capture-previous-logs.sh` gets the same treatment: its notes
now go to `capture-notes.txt` in the artifact as well as to the job log,
and a pod list that never returned is reported as such rather than as
"nothing restarted". That script now creates its own output directory
before its first line of output, which makes the comment in
`hack/e2e-chainsaw/.chainsaw.yaml` wrong about why the once-per-test
guard there holds — the only change to that file is the corrected
comment, saying the `-d` check keys on whether the catch block already
ran rather than on what the helper found.

The rest is test coverage — a new `hack/cozyreport.bats`, an extended
`hack/capture-previous-logs.bats` — and the removal of `EXIT`-trap
cleanup from the bats files those tests now cover. That trap is worth
its own note: inside an `@test` body it replaces the one the `bats`
binary installs for its own bookkeeping, so a test that then fails
prints no TAP line at all. It does not appear as `not ok`; it
disappears, and only the `# bats warning: Executed N instead of expected
M tests` line at the end distinguishes the run from a green one. A guard
test enforces the ban over a named list of files that grows as the
remaining ones are converted.

### Testing

Every collector this touches is now covered by unit tests that run
without a cluster: a new `hack/cozyreport.bats` (166 tests) and an
extended `hack/capture-previous-logs.bats` (34), driven through
`hack/cozytest.sh`. Ten `hack/*.bats` files run green locally, 265 tests
in total, with each file's `@test` count reconciled against its passing
count rather than trusted to the exit status — that reconciliation is
the point of the trap change, since an exit code answers "did anything
fail" and never "did anything run".

The guard test that bans `EXIT`-trap cleanup was checked by committing
the violation rather than by reading it: a folded `trap 'rm -rf "$tmp"'
\` + `EXIT` added to a file on its list turns the guard red and names
that file, and removing it turns the guard green again. `shellcheck
--shell=sh` over the three changed scripts reports strictly fewer
findings than before the change (`hack/cozyreport.sh` SC2086 170 to 116,
SC2162 15 to 13; the three SC2102 in `hack/cozyreport-summary.sh` are
gone), and `hack/e2e-capture-previous-logs.sh` is clean.

The parts that need a cluster — that the bounds fire, and that each one
lands in the artifact — are exercised by the e2e run itself, since these
collectors run on every failed suite.

### Screenshots

Not a UI change.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff. This PR touches `hack/cozyreport.sh`,
`hack/cozyreport-summary.sh`, `hack/e2e-capture-previous-logs.sh`,
`hack/e2e-chainsaw/.chainsaw.yaml`, ten `hack/*.bats` files and
`docs/agents/e2e-testing.md`. Nothing is moved or renamed under `hack/`,
no make target changes what it does, and the files downstream consumers
key on (`hack/package.mk`, `hack/common-envs.mk`) are untouched. No
package, values key, CRD, variant, namespace or release asset is
involved.

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

### Release note

```release-note
fix(e2e): the diagnostic report now collects per-pod evidence for pods that are Running but not Ready, which it previously skipped, and several collector sections that silently produced nothing now report correctly. Collection that stops early on a bound now says so inside the artifact, so a truncated report can be told apart from a healthy cluster.
```


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

## Summary by CodeRabbit

- **New Features**
- Diagnostic reports now prioritize relevant resources and collect
bounded pod, VM, service, storage, certificate, and Flux data.
- Reports preserve partial results and clearly identify timeouts,
unavailable resources, truncation, warnings, and collection failures.
- Previous-container logs now include bounded capture, namespace
prioritization, failure details, and partial-read evidence.
- Reports target the operator workload and safely handle missing
dependencies or temporary storage failures.

- **Documentation**
- Expanded guidance for reliable end-to-end testing and diagnostic
report collection.

- **Tests**
- Added comprehensive coverage for report generation, log capture,
failure handling, limits, and portability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-06 15:25:20 +03:00
Aleksei Sviridkin 5542ae52b7 fix(e2e): budget tenant scheduling separately from workload readiness
The tenant backend Deployment had a single 300s budget to reach
condition=Available, and two unrelated variable costs shared it. The
first is scheduling. What the suite establishes before that point is two
tenant nodes Ready, which is weaker than schedulable: a Ready node still
carries node.cilium.io/agent-not-ready until the tenant cilium agent
claims it, and a node the bringup has not finished with is
SchedulingDisabled. Scheduling took 2m18s and 1m57s in the two tenant
suites of one run, leaving the image pull to finish inside what was
left. It did not, and both suites failed with the same message for two
different shortfalls.

Wait for a node that actually accepts a toleration-free Pod before
creating the workload, on its own budget and its own failure message.
The gate encodes the scheduler's rule for such a Pod (Ready, not
unschedulable, no NoSchedule or NoExecute taint) over a custom-columns
probe, prints the node table on both outcomes so a timeout names the
taint that held it, and treats a failed probe as not-schedulable so an
API blip cannot release it. The readiness wait keeps its 300s, now
starting from a schedulable node, and dumps deployment, pod and event
state when it runs out.

Pin the workload image by digest. The tenant workers reach no registry
mirror, so nginx is pulled from Docker Hub on every run either way; the
digest fixes what that pull returns instead of leaving a floating tag
free to change size and content under a fixed deadline.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-08-06 12:20:50 +03:00
Aleksei Sviridkin 3fc8e524b2 fix(e2e): select catch pod logs by the nested release instance label
The harbor, openbao and qdrant app charts deploy their workload through
a nested HelmRelease named `<release>-system`, and pass
`fullnameOverride: <release>` down to it. The inner chart therefore
stamps app.kubernetes.io/instance from its own release name, with the
suffix, while object names come from the override, without it. The
catch blocks selected on the outer release name, so podLogs matched no
pods and every failed run collected an empty result.

A pod name says nothing about its instance label; the two come from
different sources on purpose. In qdrant the unsuffixed value is
therefore not retired but kept as a collector of its own: the parent
chart's PVC-cleanup Job labels its pod template with the parent release
name, and that suite deletes the parent inside a `try:`, so the Job is
in scope while the test still runs. The comment there is explicit about
how narrow that window is -- the hook always exits 0, so Helm calls it
succeeded and `hook-succeeded` takes the pod away; what is left is an
uninstall that hangs with Helm still waiting on it.

Harbor renders the same hook and does not get the same collector. It
has no `delete` op, so its parent release is uninstalled only in
Chainsaw's cleanup phase, which no catch can observe: catch is a defer
inside the step, while the cleaner runs from t.Cleanup, after the test
function returns. That collector would be the same permanently-empty
capture this commit removes. A comment records the asymmetry so the
next reader does not restore it for symmetry. openbao renders no such
hook at all.

Each workload value is now also pinned on a resource in an assert the
suite already runs, so the chart renaming a label fails a step instead
of quietly emptying a capture. The comments state what that does and
does not cover: it does not catch a wrong value typed into a selector,
and it catches nothing when the run fails before the assert.

Harbor's database was reached by no collector at all. It is selected on
`cnpg.io/cluster`, CNPG's own primary label, rather than on the
instance label CNPG merely derives from the Cluster name -- the primary
label has nothing to drift from, and it is what the postgres suite and
the WorkloadMonitors already use.

Each lookup keeps stderr apart from the value it captures. kubectl exits
0 while logging a klog line about an API group it could not reach, and
an aggregated APIService in that state is what a failed run usually
looks like; folded into the value, that line makes the "nothing found"
branch unreachable and prints as though it were a result.

Telling those outcomes apart is one helper in hack/e2e-chainsaw/_lib/
rather than a copy per call site, separated from the kubectl call so it
can be pinned without a cluster, the way etcd-probe.sh already is. Both
the census and the qdrant cleanup listing report through it, differing
only in wording, so the discrimination is implemented and tested once.
The tests cover each outcome for both wordings, including the zero exit
with a klog line on stderr -- the case a folded stream turned into a
row.

A collector that matches nothing cannot say so: podLogs is silent, and
`kubectl get -l` exits zero on an empty match. Each test-wide catch
prints the instance label of every pod in the namespace, and the qdrant
cleanup catch reports its three outcomes apart from a single call --
printing what that call returned rather than running it again, which
could put a healthy listing under a failure heading.

That census runs on a degraded cluster, where kubectl otherwise retries
discovery against an unreachable apiserver until something kills the
script, so every lookup carries `--request-timeout`. Measured against an
unreachable apiserver, a bounded call costs about five times the flag,
since kubectl retries discovery five times before it gives up. Each
script then carries an explicit op timeout sized above its own calls --
2m for the single-call censuses, 5m for the qdrant cleanup listing,
which makes three. An inner budget is what should fire: a SIGKILL at the
op boundary yields nothing, where a kubectl that gives up leaves an
error to print.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-08-05 16:46:50 +03:00
Aleksei Sviridkin d3ae21b7f0 fix(e2e): name every way the previous-instance capture falls short
The capture wrote its reasons to the job log only. A reader holding the
uploaded artifact saw N logs in a directory with no way to tell N from
all of them: the container cap that dropped the rest, a pod list that
never returned, a previous instance that produced nothing, and a read
the outer backstop cut short all looked alike from outside. Every one
of those lines now goes to capture-notes.txt as well, and the directory
is created before the first of them, so a capture that found nothing is
distinguishable from one that never ran.

A pod list that failed is no longer reported as nothing having
restarted. That is a claim about the cluster which the script never
observed, and it is the claim most likely to talk a triager out of the
crash-loop hypothesis on exactly the run where it is right.

Exit 137 no longer asserts whose signal ended the read. 124 comes from
timeout and from nothing else, but 137 is 128+SIGKILL, which the kill
grace produces and so does the OOM killer or a teardown signalling the
process group. Both belong in the cut-off branch, since the capture
ends early either way, but reported flatly as the script's own timeout
a read killed at second two reads as one that waited the full eighteen.

COZY_PREVLOG_TAIL rejects a value kubectl cannot use and says so. A
zero tail asks for no lines while kubectl still exits 0, which the
empty-output branch would otherwise report as the container having
logged nothing, discarding the only remaining copy of a log the kubelet
has already collected. A leading zero is rejected because `$(( ))`
reads it as octal while `[` reads it as decimal, so one string means
two numbers. Each kept log names the bound it was written under, since
--tail drops the oldest lines and nothing else in the output says so,
and -1 suppresses that note because no bound was applied.

An output directory that cannot be written is reported as the local
condition it is. It arrives as an argument, so it can be a file, or
missing, or unwritable; the redirection then fails before kubectl runs
and hands the shell's own status to the branch that names kubectl,
which reported "no previous instance retrieved (kubectl exit 2)" once
per container.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-08-05 16:03:57 +03:00
IvanHunters 0612a538a3 fix(e2e): bound the Talos-capture failure-branch forensic kubectl calls
The describe/get-events/get-certificaterequests calls that run when the
reader Certificate or the diagnostics Pod misses its readiness wait were
unbounded, yet those branches fire precisely when the management apiserver is
wedged. A stalled forensic call there can burn minutes of the 50m step budget
before the pre-existing (a2)/(c) diagnostics and the EXIT-trap tenant
snapshot run. Add --request-timeout=30s to them, matching the happy-path
capture calls.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-08-05 15:13:56 +03:00
IvanHunters cfbfd770b8 fix(e2e): bound the tenant Talos capture kubectl calls
The in-guest diagnostics chain runs only when the sandbox is already
degraded enough to miss the 18m node-join deadline (the documented
management-etcd fsync-storm regime), yet its kubectl legs were unbounded
while every talosctl leg is wrapped in timeout. A stalled apiserver call in
the (b) capture could consume the remaining step budget before (a2), (c) and
the EXIT-trap tenant crust-gather run, so a diagnostics feature could reduce
total diagnostic yield under the exact conditions it targets.

Add --request-timeout=30s to the raw Certificate/Pod apply, the three
reader-Secret reads and the worker VMI list, and route the Certificate Ready
wait through kubectl_wait_retry so a single transient etcd-leader flap
retries instead of aborting the whole capture. Update the bats assertion for
the reworded wait call. Also correct a stale comment: the chainsaw step
timeout is 50m, not 40m.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-08-05 14:47:49 +03:00
Myasnikov Daniil 60e44b24f1 fix(e2e): make tenant Talos diagnostics reliable
Fix admission, capture setup, timeout headroom, and portability.

Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-05 14:52:31 +05:00
Myasnikov Daniil 80152b1a07 fix(e2e): capture Talos logs for tenant worker join failures
Tenant Kubernetes worker join failures currently stop at host-side VMI
and CSR state because the worker-only topology does not materialize a
tenant talosconfig.

Mint a short-lived os:reader client certificate and run talosctl from a
hardened same-namespace Pod after the Ready deadline. Preserve bounded
dmesg and kubelet captures for every worker without masking the original
test failure.

Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-05 13:18:20 +05:00
Andrei Kvapil 330acfe6e2 fix(e2e): widen tenant node-join deadline to 18m
The kubernetes-latest and kubernetes-previous suites flaked on the
worker node-join wait under host load. In run 30260770694 both failed at
exactly 12m with zero tenant Nodes registered and the tenant cilium HR
still mid-install: a transient drbd.linbit.com/lost-quorum taint delayed
the worker DataVolume imports, so the KubeVirt VMIs were only created ~2m
into this wait and the guests then had ~10m to import Talos, boot,
register a kubelet and let cilium turn the nodes Ready.

This single deadline absorbs the entire worker bring-up (the
machinedeployment replicas=2 gate clears before the VMIs even exist), so
under storage pressure 12m is too tight. A less-loaded fleet run passed
both suites unchanged, confirming load-induced slowness rather than a
stuck bring-up. Widen the deadline to 18m, which restores margin and
still sits well inside the 40m chainsaw step timeout.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-07-28 17:48:40 +02:00
Andrei Kvapil b22b7b2431 test(e2e): make kuberture split-horizon probes survive Cilium ingress-IP race (#3430)
## What this PR does

The split-horizon-routing-proof step ran two external-dns probes as bare
Pods and polled them by fixed name. When the Cilium host-scope IPAM plus
Gateway API restore race (cilium/cilium#46799) hands a probe the
per-node reserved:ingress IP, the sandbox is rejected with "IP already
in use" and the e2e leak-healer deletes the wedged pod, expecting a
controller to reschedule it onto a free IP. A bare Pod has no
controller, so that delete was terminal: the probe vanished and the step
failed on a NotFound pod. It surfaces as an intermittent kuberture E2E
failure with a `FailedCreatePodSandBox` event on a probe pod.

This runs the probes as Jobs. The Job re-creates its pod on a fresh IP
after the healer deletes it, so the proof survives the race. Each Job is
asserted to reach Complete instead of polling a fixed pod name, and its
logs are read by the job-name label. The failure path also gains a catch
podLogs selector for the probe pods, so their stdout is captured when a
probe fails for a different reason.

### Screenshots

None. No UI changes.

### Downstream repositories

The diff touches only the kuberture E2E suite under
`hack/e2e-chainsaw/kuberture/`. No package, values schema, chart, CRD or
platform manifest changed. Nothing in the downstream trigger map is
reached.

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

### Release note

```release-note
NONE
```


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

* **Bug Fixes**
* Improved end-to-end test reliability by hardening the split-horizon
routing proof against probe workload churn.
* Strengthened route isolation validation between public and internal
probes.
* Enhanced failure diagnostics by capturing logs from the completed
probe Jobs.
* **Testing**
* Updated DNS EDNS probes to run as Kubernetes Jobs (instead of bare
Pods) to support retries and rescheduling.
* Added explicit assertions that both public and internal probe Jobs
reach **Complete** before running hostname/route isolation checks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-28 17:43:26 +02:00
Aleksei Sviridkin 436a19df1b test(e2e): make kuberture split-horizon probes survive Cilium ingress-IP race
The split-horizon-routing-proof step ran two external-dns probes as bare
Pods and polled them by fixed name. When the Cilium host-scope IPAM +
Gateway API restore race (cilium/cilium#46799) hands a probe the per-node
reserved:ingress IP, the sandbox is rejected with "IP already in use" and
the e2e leak-healer deletes the wedged pod, expecting a controller to
reschedule it. A bare Pod has no controller, so the delete was terminal:
the probe vanished and the step failed polling a NotFound pod.

Run the probes as Jobs so the pod is re-created on a fresh IP after the
healer's delete, assert each Job reaches Complete instead of polling a
fixed Pod name, and read the logs by job-name label.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-27 22:59:49 +03:00
Aleksei Sviridkin a7ce4371c2 docs(agents): correct the chainsaw podLogs capability note
The e2e conventions, the previous-logs capture script, and the global
chainsaw catch all claimed the v0.2.15 podLogs collector exposes only
container/tail/selector. It also accepts name, namespace, timeout and
the cluster fields; only the previous option is genuinely missing. The
wrong list steers catch authors toward imperative scripts for
cross-namespace log capture that podLogs handles declaratively.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-27 22:48:25 +03:00
Aleksei Sviridkin 824a799c6a test(e2e): capture redis convergence diagnostics in harbor suite
When harbor's redis sentinels never converge, the suite times out on
the HelmRelease assert with no evidence of why: the catch pod-log
selector matches neither the redis-operator (it runs in another
namespace) nor the rfr-/rfs- pods, and the RedisFailover CRD ships no
status subresource to describe-assert on. Add a RedisFailover describe
plus a best-effort script that dumps the operator log and the
rfr-/rfs- pod states and logs, so the stalled reconcile gate is
visible in the failure output.

Also widen the harbor-test HR wait from 6m to 10m: readiness is gated
on the operator electing a redis master, which normally takes 2-3m but
runs long on a CPU-committed runner. This matches the
harbor-test-system budget in the same suite and the justified longer
waits allowed by the e2e conventions.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-27 22:48:24 +03:00
Aleksei Sviridkin 6ecb36ffc7 feat(cozystack-controller): extract application CA into a key-free tenant Secret (#3407)
## What this PR does

Adds an engine-agnostic controller that publishes a key-free
`<release>.tenant-ca` Secret (only `ca.crt`) for a managed application,
so a tenant can verify the application's TLS without ever seeing a
private key. The source is declared, not guessed: a chart renders a
namespaced `TenantProjection` sentinel (group `internal.cozystack.io`)
naming the CA Secret to lift; the controller watches the sentinel,
extracts `ca.crt`, and writes the projection owner-referenced to the
sentinel, so garbage collection is native. The projection carries the
`internal.cozystack.io/tenant-ca` label the lineage webhook turns into
tenant visibility. Postgres is the first consumer.

The trust boundary is RBAC: no tenant role grants any verb on
`internal.cozystack.io`, so a tenant cannot forge a sentinel. A
companion `ValidatingAdmissionPolicy` that pins the writer to
helm-controller ships separately as defence in depth.

This is the controller-and-CRD slice, split out from #3299 for focused
review. It is the only piece required for the feature to work; the
render-time helper, the writer policy, the capability-gate, and the
release-prefix validation land as their own PRs.

### Downstream repositories

Walked the trigger map against the diff. This adds an internal CRD and
controller and a sentinel to the Postgres chart; no app is added or
renamed and no `values.schema.json` changes, so the typed provider and
Ansible are unaffected. The one to confirm is the Postgres README edit —
if the website mirrors it, it needs a refresh.

- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:

### Release note

```release-note
feat(cozystack-controller): publish a key-free per-application CA trust anchor (`<release>.tenant-ca`) that tenants read to verify managed-application TLS, declared through a new `TenantProjection` CRD. Postgres is the first consumer.
```


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

## Summary by CodeRabbit

* **New Features**
* Added tenant-facing CA certificate projections for PostgreSQL
deployments.
* Tenant secrets now provide only the CA certificate, excluding private
keys.
  * Added support for configuring and monitoring tenant CA projections.

* **Documentation**
* Updated PostgreSQL TLS instructions to retrieve certificates through
tenant secrets.

* **Bug Fixes**
* Improved protection against invalid certificates, secret collisions,
and accidental exposure of private key material.
* Added validation to ensure projected certificates remain synchronized
with their sources.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-23 14:14:28 +03:00
Andrey Kolkov 8aeedde088 feat(backups): support PostgreSQL point-in-time recovery (PITR) (#3383)
## What this PR does

Makes PostgreSQL point-in-time recovery (PITR) a genuinely supported,
tested capability for the new `backups.cozystack.io` API. Fixes #2774.

Was **stacked on #3313** (`fix/3300-cnpg-barman-plugin`) for the
`recoveryTime` → CNPG `bootstrap.recovery.recoveryTarget.targetTime`
plumbing; #3313 has since merged and this branch is rebased on `main`.
This PR adds the reliability + verification layer #2774 asked for.

### Changes

- **Precise failure reason for an unreachable target.** The restore
deadline (`spec.options.restoreTimeoutSeconds`, default 30m) stays the
sole authority for "this restore is stuck" — the client waits the full
window either way. What changes is the *diagnosis*: once the deadline
has elapsed, if `recoveryTime` was set and the recovery Cluster never
went healthy, the CNPG driver reads the `full-recovery` pod logs and —
when several distinct attempts exited with PostgreSQL's `FATAL: recovery
ended before configured recovery target was reached` — fails the
RestoreJob with `RecoveryConverged=False` / `Ready=False` (reason
`RecoveryTargetUnreachable`) and an actionable message, instead of a
bare generic timeout. Timing is unchanged; only the reason/message
differ. (For a faster rejection, set a shorter `restoreTimeoutSeconds`.)
Design points:
- **We deliberately do NOT trip early off the FATAL.** A valid near-now
target emits that same FATAL transiently — repeatedly, for many
attempts, on a slow archiver — before it converges, so any earlier trip
would wrongly reject a recoverable restore (observed twice in CI). The
FATAL is used only to *explain* a failure the deadline has already
declared.
- **Health wins unconditionally** — the deadline/classification check
precedes the healthy check, but a recovery that converges within the
window reaches success and is never classified as failed (a live dev7
restore did exactly this).
- **Counting distinct FATAL-ing attempts** (not pod failures) tolerates
a one-off archive-lag miss on a near-now target and ignores transient
recovery-pod crashes (a node blip / brief API-server unreachability
never emits that FATAL).
- Adds the `pods/log` RBAC grant the log read needs (distinct
subresource from `pods`); a `Forbidden` is logged loudly and surfaced in
the failure message rather than silently disabling the classification.
- **e2e** (`examples/backups/postgres/run-all.sh`, wired into the
Chainsaw `postgres-2-backup-roundtrip` suite):
- Step 45 — write `before`, capture server timestamp (µs precision),
write `after`, restore to that timestamp, assert `before` present and
`after` absent.
- Step 46 — a `recoveryTime` an hour ahead must fail with reason
`RecoveryTargetUnreachable` once the deadline elapses (asserts the
*reason*, so it proves the classification fired, not just that a timeout
occurred).
- Chainsaw budget raised 25m → 40m to cover three sequential restores +
the post-deadline classification.
- **Docs + example**: uncommented `45-restorejob-pitr.yaml`; a
`docs/operations/backup-classes.md` PITR section (recoverable window,
discovering earliest/latest restorable time from the backup catalog
since `firstRecoverabilityPoint` is unreliable under the plugin,
archive-lag tolerance, idempotency under GitOps).

### Verification

- `go build` / `go vet` clean; unit tests pass
(`logIndicatesRecoveryTargetUnreachable`, `recoveryUnreachableFromLogs`,
`recoveryPodsToInspect`, nil-Clientset guard, a reconcile-level
success-path test asserting `Succeeded` + `RecoveryConverged=True`, and
— added per review —
`TestReconcileCNPGRestore_DeadlineClassifiesRecoveryTargetUnreachable`,
a reconcile-level test driving the deadline branch through all three
sub-paths: FATAL found → reason `RecoveryTargetUnreachable`, FATAL
absent → generic timeout, `Forbidden` log read → generic timeout +
missing-`pods/log`-RBAC note. It fails if the call-site reason is
reverted to the generic one).
- Reviewed via adversarial branch-review; fixes include a critical
missing-RBAC dead-on-arrival, a classification false-positive on
transient recovery-pod crashes, and a sub-second e2e flake.
- **dev7**: backup + WAL archiving + recovery convergence verified live
on the #3313 build. The load-bearing assumption of the classification —
CNPG **accumulates** failed `full-recovery` pods rather than reusing one
— was confirmed (observed 7 simultaneous failed recovery pods). A
fully-green live step-46 run was blocked by intermittent COSI
bucket-provisioning flakiness on dev7; the negative path is covered by
the CI Chainsaw e2e.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff. The only user-facing surface is the new
`docs/operations/backup-classes.md` PITR section; the website keeps a
hand-mirrored copy per docs version.

- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up: PITR docs mirror warranted (not yet opened; same pending
mirror as #3313's backup-classes changes)
- [x] No other downstream repository is affected by this change

```release-note
feat(backups): PostgreSQL RestoreJob now supports point-in-time recovery (PITR). Set spec.options.recoveryTime (RFC3339) to restore to an exact instant. A recoveryTime past the latest archived WAL still runs to the restore deadline (spec.options.restoreTimeoutSeconds, default 30m), but the failure is now reported with the precise reason RecoveryTargetUnreachable and an actionable message instead of a generic timeout; set a shorter restoreTimeoutSeconds for a faster rejection.
```

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

* **New Features**
* Added PostgreSQL point-in-time recovery (PITR) support via RFC3339
`recoveryTime`.
* Introduced a PITR RestoreJob example, plus helper variables and an
unreachable-target scenario.
* **Bug Fixes**
* Improved restore deadline handling with deterministic, log-based
fail-fast classification for unreachable recovery targets, including
clearer conditions.
* **Documentation**
* Added detailed PITR operational documentation (recovery windows,
failure behavior, and GitOps/idempotent behavior).
* Expanded README and example comments for PITR verification and failure
expectations.
* **Tests**
* Extended unit tests and end-to-end chainsaw workflow to cover PITR
success and unreachable failures.
* **Chores**
* Updated RBAC to allow pod log reads; refined cleanup and run scripts
for additional RestoreJob artifacts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-23 14:14:58 +04:00
Myasnikov Daniil 4e0cfd994d test(e2e): render COSI catch output as YAML
Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-22 17:27:02 +05:00
myasnikovdaniil 55195ded0c test(e2e): capture previous container logs and stop killing the snapshot (#3346)
## What this PR does

Root-causing a recent E2E failure was nearly blocked because the
diagnostics captured on failure had collected almost nothing. Two
independent mechanisms were responsible, and both had been silently in
place for a long time.

**The failing container's previous logs were never captured.** The pod
had crash-looped, so the decisive evidence lived in the first container
instance, reachable only through `kubectl logs --previous`. Chainsaw's
`podLogs` action has no `previous` field, so this could not be expressed
per-suite; the conclusion had to be reconstructed from indirect evidence
instead. A new capture runs from the global catch and from the BATS
failure hook, selecting containers at runtime by `restartCount > 0`
across both init and regular containers, so a healthy run stays quiet
and future suites are covered without per-suite wiring.

**The catch was structurally guaranteed to be killed.** A Chainsaw
`script` op is bounded by `timeouts.exec`, which is 5m, while the
catch's own inner budgets summed to roughly 960s. It reported `SCRIPT |
ERROR | signal: killed` and produced nothing — and its second leg, the
data-plane capture, had therefore never run at all. The kill signature
was itself misleading: `timeout` exits 124 and cannot produce that
message, which comes from Go's `exec.Cmd` killing the process group.
Each catch op now carries an explicit timeout above the budgets it
contains, so the inner one fires and leaves a partial, usable artifact
instead of losing everything.

**The snapshot was truncated at 60 seconds.** `crust-gather --duration`
defaults to 60s and was never passed, so every snapshot ever taken was
cut short, with nothing in the artifact saying so. The API discovery it
performs first is not covered by that budget at all, which is where the
outer wall-clock was going on a cluster with a degraded aggregated
APIService — precisely the state a failed run is in. Both budgets are
now set explicitly, and the collector's output goes to a log inside the
snapshot with a completeness verdict, so "is this snapshot complete?" is
answerable from the artifact.

Failure paths are reported rather than swallowed: an unreachable API
says so instead of implying nothing had restarted, a truncated capture
is kept and marked, and every dropped item is logged, since silent
truncation reads as full coverage.

### What is not verified here

The mechanism claims are read from the pinned versions of Chainsaw and
crust-gather. Everything cluster-facing is not verified by this PR: the
go-template's output shape against a live API server, `--previous`
returning the first instance for a real crash-looper, and whether the
new collection budgets suffice on a degraded cluster. The bats suite
covers the selection helpers only, and those cases are mutation-tested.

### Release note

```release-note
NONE
```


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

* **Diagnostics**
* Improved end-to-end failure evidence by capturing “previous instance”
logs for crash-looping pods (including restarted init containers) and
prioritizing relevant namespaces.
* Added time-bounded diagnostics collection with explicit outer
timeouts; collector output is saved to per-snapshot log files.
* Classifies collector outcomes as complete, timeout-truncated, or
failed, while preserving the original test failure signal.
* **Documentation**
* Expanded E2E failure-diagnostics guidance with new timeout and
evidence-capture conventions.
* **Tests**
* Added unit-style tests for previous-log selection, prioritization, row
limiting, and deterministic log artifact naming.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-22 17:22:11 +05:00
Andrey KolkovandClaude Opus 4.8 4605bbf3db test(mariadb): pre-clean the backup roundtrip against stale tenant-root state
Address review feedback on #3384. The TIA rule for
examples/backups/<app>/ that an earlier revision of this commit added
has since landed on main independently (50fb3c87c, "run the postgres
backup round-trip e2e in CI under TIA"), so only the suite change
remains here after the rebase.

mariadb-2-backup-roundtrip runs in the shared, persistent tenant-root
namespace with fixed resource names, so a run killed before the finally
leaves stale state behind; a leftover BackupJob still carrying
status.phase=Succeeded would satisfy the Succeeded wait without a real
backup ever running. Add an idempotent pre-clean step (run cleanup.sh
before run-all.sh), mirroring the etcd suite, and reword the suite
comment to reference the merged etcd-2-backup-roundtrip pattern instead
of postgres-2-backup-roundtrip.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-22 10:45:32 +04:00
Andrey KolkovandClaude Opus 4.8 d1f2822018 test(mariadb): add backup/restore round-trip e2e (chainsaw) via example harness
Reworks the never-merged backup-mariadb-e2e branch onto the modern
Chainsaw approach (docs/agents/e2e-testing.md). The bats port that
branch carried (hack/e2e-apps/backup-mariadb.bats) is dropped — the
whole hack/e2e-apps/*.bats app suite has been replaced by
hack/e2e-chainsaw/, and the mariadb strategy controller + example
manifests it depended on have since landed on main independently. Only
the e2e coverage was still missing.

Convert examples/backups/mariadb/ from static numbered YAML into the
same executable harness the other backup demos use (00-helpers.sh,
run-all.sh, cleanup.sh), keeping the numbered manifests as the
human-readable flow that run-all.sh drives — so the documented flow and
the automated test can never drift. run-all.sh provisions a Bucket,
derives the two per-app Secrets the strategy references from BucketInfo
+ the seaweedfs CA, deploys a source MariaDB with a sentinel row, fires
an ad-hoc BackupJob, restores to a copy with a to-copy RestoreJob, and
asserts the sentinel round-tripped through S3 into the copy while the
source is left untouched.

Add a second Chainsaw Test (mariadb-2-backup-roundtrip) to
hack/e2e-chainsaw/mariadb/ that drives run-all.sh + cleanup.sh, mirroring
postgres-2-backup-roundtrip: runs in tenant-root (the isolated e2e tenant
cannot reach tenant-root's seaweedfs across the Cilium egress policy),
skipDelete since the harness (not Chainsaw) owns the resources, fail-fast
on terminal BackupJob/RestoreJob phases, and scoped catch diagnostics.
Cleanup also prunes the operator-side k8s.mariadb.com Backup/Restore CRs
the Cozystack jobs own only by label.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-22 10:44:42 +04:00
Andrey KolkovandClaude Opus 4.8 6a1e2d0d06 feat(backups): support PostgreSQL point-in-time recovery (PITR)
Makes RestoreJob.spec.options.recoveryTime a genuinely supported, tested
capability for the new backups.cozystack.io API (issue #2774). The
recoveryTime -> CNPG bootstrap.recovery.recoveryTarget.targetTime plumbing
already existed on the barman-cloud-plugin branch; this adds the reliability
and verification layer the issue asked for:

- Fail-fast for an unreachable target: when a RestoreJob has recoveryTime set
  and the recovery Cluster is not healthy, the driver reads the CNPG
  full-recovery pod logs and, once several distinct attempts have exited with
  PostgreSQL's "recovery ended before configured recovery target was reached"
  FATAL, fails the RestoreJob with conditions RecoveryConverged=False /
  Ready=False (reason RecoveryTargetUnreachable) instead of hanging to the
  restore deadline. Health is checked first, so a converged recovery always
  wins; counting distinct FATAL-ing attempts (not pod failures) tolerates a
  transient archive-lag miss on a near-now target and ignores transient
  recovery-pod crashes. Adds the pods/log RBAC grant the log read requires.

- e2e (examples/backups/postgres/run-all.sh, wired into the Chainsaw
  postgres-2-backup-roundtrip suite): step 45 writes a before marker, captures
  the server timestamp, writes an after marker, restores to that timestamp and
  asserts the before row survived and the after row did not; step 46 submits a
  recoveryTime an hour ahead and asserts it fails fast with reason
  RecoveryTargetUnreachable.

- Working, uncommented example (45-restorejob-pitr.yaml) and a docs section
  (docs/operations/backup-classes.md) covering the recoverable window, how to
  discover the earliest/latest restorable time from the backup catalog, the
  archive-lag tolerance, and idempotency under GitOps.

Refs #2774

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-22 10:40:18 +04:00
Aleksei Sviridkin 99f04d99bd test(postgres): verify the tenant CA projection end to end
Assert the live controller publishes <release>.tenant-ca carrying only
ca.crt, that its certificate matches CloudNativePG's CA while the private
key never crosses over, and that the anchor is reachable through the
tenantsecrets API while the key-bearing source Secret is not.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-21 23:59:48 +03:00
Aleksei Sviridkin 76f7267ff2 test(e2e): describe --previous as the preceding instance, not the first
kubectl logs --previous returns the immediately preceding terminated
container instance, not the original first one — after multiple restarts
the first instance is already gone. Reword the helper, the BATS and
Chainsaw failure handlers, and the diagnostics doc to match the actual
--previous semantics.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-21 18:57:01 +03:00
Aleksei Sviridkin 1ff83f5c06 test(e2e): fix false claim that a job timeout preserves the report
The catch-timeout comment justified the raised ceiling by asserting that
if: always() upload survives a job timeout. It does not: a 180-minute cap
ends the job cancelled, and the post-cancellation grace window is consumed
by the earlier report-collection step, so both cozyreport.tgz and the job
log are lost. Correct the comment to describe the real failure mode.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-21 18:56:19 +03:00
Aleksei Sviridkin 07c67a8913 test(e2e): capture previous container logs and stop killing the snapshot
Root-causing a crash-loop failure was blocked twice over by the
diagnostics collected on failure.

`kubectl logs` shows only the current container instance, so for a
crash-looping pod the suites' `podLogs` collector captured the Nth
restart -- a fresh replay of the same startup path -- while the
evidence that explained the failure sat in the first, interrupted
instance. Chainsaw's podLogs collector has no `previous` option, so
add e2e-capture-previous-logs.sh and drive it from the global catch
and from the BATS failure hook. It discovers containers with
restartCount > 0 at runtime (init containers included, where an
interrupted bootstrap lives), orders the app suites' namespace first,
and dumps each to the job log and the snapshot tree. Containers that
never restarted are skipped, so a healthy run emits one line rather
than a page of "previous terminated container not found".

Every way that capture can come up short is named rather than
inferred, because kubectl exits 1 for a garbage-collected container,
an RBAC denial and an unreachable node alike: the container cap, a
pod list that never returned (which is not the same as nothing having
restarted), a read that timed out versus one kubectl refused and the
reason it gave, and a previous instance that produced no output. A
partial capture is kept and marked truncated rather than deleted --
it is the only copy of a log already gone from the cluster.

The crust-gather snapshot was killed by the Chainsaw op containing it.
A script op is bounded by timeouts.exec (5m), and the catch's own
wall-clock wrappers summed to 960s, so the script was SIGKILLed
partway through -- taking out the process group mid-write and skipping
the data-plane capture entirely. Give each catch op an explicit
timeout above the budgets it contains, so an inner timeout fires
instead and leaves a partial, still-servable snapshot behind. The
data-plane leg drops from 600s to 300s at that call site so both legs
fit one envelope; the BATS hook keeps the wider budget, having no
containing op.

Set crust-gather --duration explicitly as well. It is the collection
phase's own budget, it defaults to 60s, and on elapse it abandons the
collection and skips its finish step, leaving a truncated tree with no
indication anything is missing. The outer wall-clock has to exceed it
because the API discovery that runs first is not covered by that
budget at all -- with an unhealthy aggregated APIService, the state a
failed run is usually in, discovery alone can consume the whole
timeout before a single object is written. Send each collector's
output to a log beside its snapshot instead of /dev/null, with a
complete/truncated/failed verdict, so that question is answerable
from the artifact.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-21 18:37:07 +03:00
Andrey Kolkov e73ad68a85 fix(backups): support cnpg-operator with barman plugin (#3313)
<!-- 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 `backport` if it's a bugfix that needs to be backported
to a previous version.
-->


## What this PR does

Fixes #3300. Migrates CNPG Postgres backups off the deprecated native
`spec.backup.barmanObjectStore` (removed in CNPG 1.29; the
`barman-cloud-*` binaries it needs are absent from the `standard` image
variant keycloak-db pins, and the bare system-flavor tags apps/postgres
pins are deprecated upstream) onto the [CloudNativePG Barman Cloud
plugin](https://github.com/cloudnative-pg/plugin-barman-cloud), across
every Postgres backup surface: `apps/postgres`, `system/keycloak`, and
the `cozy-default-cnpg` BackupClass strategy driver. The chart renders
`spec.plugins` + a `barmancloud.cnpg.io/ObjectStore` CR; the
backup-controller driver SSA-applies an `ObjectStore` and patches
`spec.plugins`; `Backup`/`ScheduledBackup` use `method: plugin`. The
`postgres-operator` chart vendors and co-locates the
`plugin-barman-cloud` operator (with its cert-manager-issued mTLS) so
CNPG can discover it.

One platform-level change is required for the plugin to run and ships
with this PR (see the upgrade notes in
`docs/operations/backup-classes.md` and the release note below): the
CloudNativePG operator is bumped to 1.28.1 (also fixing an operator/CRD
version skew that broke all backups). The other prerequisite — the
LINSTOR scheduler admission webhook stripping
`initContainers[].restartPolicy` cluster-wide, which broke the plugin's
restartable sidecar — is already fixed on `main` (f4aef7305 re-vendors
`linstor-scheduler` to upstream chart 0.3.1 / extender v0.3.6, which
keeps the webhook and stops stripping unknown pod fields), so after the
rebase this PR no longer touches `linstor-scheduler`; just do not hold
that package back on an older version when rolling out the plugin.

Supersedes #3306 (the keycloak `system`-image workaround): keycloak-db
now backs up via the plugin on the `standard` image, so that stopgap is
no longer needed.

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

- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up: docs/operations/backup-classes.md changed; a website mirror
follow-up is warranted (not yet opened).
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

<!--  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
fix(backups): migrate CNPG Postgres backups to the CloudNativePG Barman Cloud plugin

Postgres backups (apps/postgres, system/keycloak, and the cozy-default-cnpg BackupClass strategy) now use the CloudNativePG Barman Cloud plugin (spec.plugins + a barmancloud.cnpg.io/ObjectStore, method: plugin) instead of the deprecated native spec.backup.barmanObjectStore (removed in CNPG 1.29; the barman-cloud-* binaries it needs are absent from the standard image variant keycloak-db pins, and the bare system-flavor tags apps/postgres pins are deprecated upstream). The barman-cloud plugin operator is installed alongside the CNPG operator.

One platform-level change rides along and requires operator attention on upgrade:
- The CloudNativePG operator is bumped to 1.28.1 (fleet-wide minor upgrade); this also fixes an operator-newer-than-its-CRDs skew that made the operator report "instance manager was restarted during backup" and fail every backup. The vendored chart ships the CNPG CRDs as ordinary release manifests (crds.create), so helm upgrade updates them with the operator; the skew came from the image.tag pin outrunning the chart's CRDs, which this PR removes.
- The plugin's restartable sidecar requires the linstor-scheduler admission webhook that no longer strips initContainers[].restartPolicy (upstream chart 0.3.1 / extender v0.3.6, shipped separately on main); do not hold the linstor-scheduler package back on an older version when rolling out the plugin.
```

Close #3300, #3246

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

* **New Features**
* Switched PostgreSQL CNPG backups/restores to the plugin-based Barman
Cloud workflow, using dedicated ObjectStore resources for backup and
recovery.
* Added the Barman Cloud CNPG-I plugin chart (deployment, TLS, RBAC,
leader election).
* **Bug Fixes**
* Improved backup/restore attachment messaging when the CNPG Cluster
isn’t ready yet.
* Preserves server-name behavior to avoid WAL-archive prefix issues;
clarified endpoint CA handling.
* **Changes**
* Helm integration no longer uses the legacy
`spec.backup.barmanObjectStore` path; updated gating, credential
projection, and ObjectStore-related RBAC/cleanup.
* Removed the linstor-scheduler admission webhook; updated
CloudNativePG/operator versions and CRDs.
* **Tests**
* Updated controller and Helm-unittest checks for the plugin flow;
added/extended end-to-end backup/restore coverage.
* **Documentation**
* Expanded upgrade/migration and backup documentation; updated backup
example materials and comments.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-21 14:45:52 +04:00
Andrey KolkovandClaude Fable 5 b897b53107 test(postgres): fix S3 CA secret name in backup roundtrip e2e
The backup roundtrip e2e copied the seaweedfs S3 CA from a Secret named
seaweedfs-system-ca-cert in tenant-root, a name captured off a dev
cluster. On current main the seaweedfs chart pins
fullnameOverride: seaweedfs, so the cert-manager CA Certificate's secret
is seaweedfs-ca-cert; the old name is absent and the first CI run of the
e2e failed at step 05a with 'secrets "seaweedfs-system-ca-cert" not
found'.

Default S3_CA_SECRET to seaweedfs-ca-cert and, when that Secret is
absent, auto-discover the seaweedfs CA Certificate (the seaweedfs-labelled
one with spec.isCA=true) and read its secretName, so a future
fullnameOverride change does not silently break the copy again. Fail with
a clear message when neither resolves. README and chainsaw comment
updated to match.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-20 22:31:27 +04:00
myasnikovdaniil aa707f17b5 test(etcd): stop losing the metrics probe's output to the attach race (#3350)
## What this PR does

The etcd suite's metrics contract failed in CI with `metrics endpoint
did not return 200 over http on the 'metrics' port; got: If you don't
see a command prompt, try pressing enter.`

The probe runs a one-shot curl Pod and reads a `code=<status>` marker
back off its output via `kubectl run --rm --restart=Never --attach`.
`--attach` only establishes its connection once the Pod reaches Running,
so a container that exits in milliseconds — which is what curl against a
Pod IP does — writes its stdout into the void, and `--rm` then destroys
the Pod carrying the log that still held it. The step reports the
metrics endpoint as broken on a cluster that was serving it correctly
throughout.

The evidence that this was output loss rather than an endpoint failure
is that the captured output contained no `code=` at all. A genuine
connection failure would still have printed `code=000` from `-w`, plus
curl's own error text from `-fsS`, and both were being captured.
Meanwhile the probe Pod's events show a clean run, all three etcd
members were up without restarts, and every preceding assert in the
suite passed.

The fix drops `--rm`, re-reads the Pod log when the attach stream
carried no marker, and deletes the Pod explicitly afterwards. This is
the same treatment `hack/e2e-chainsaw/_lib/talos-image-cache.sh` already
applies to the identical race — that probe documents it inline; the etcd
one was simply never given it. A `--pod-running-timeout` is set for the
same reason it is there: the probe image is used nowhere else in the
tree, so it pays a cold pull on every run and kubectl's one-minute
default was a second way to end up with no marker.

The recovery cannot turn a real failure green. The marker encodes the
outcome, so a `code=503` recovered from the log still fails the step; an
attach stream that already carried a marker is never second-guessed; and
the pre-run delete blocks, so a stale Pod's log cannot be read in place
of this run's. Verified by running the probe's exact curl invocation
against a 503 and against a dead port: the first yields `code=503`, the
second `code=000` plus curl's error, and both fail. The probe still runs
exactly once — this is output recovery, not a retry.

Helper extracted to `hack/e2e-chainsaw/_lib/etcd-probe.sh` with unit
coverage in `hack/etcd-probe_test.bats`.

### Release note

```release-note
NONE
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved etcd metrics probe reliability by recovering probe results
when attached output is incomplete.
* Preserved meaningful probe and Kubernetes error details, including
non-success status codes.
* Improved probe pod timing and cleanup to reduce intermittent
end-to-end test failures.

* **Tests**
* Added coverage for successful recovery, failed probes, missing output,
and log retrieval errors.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-20 12:26:08 +05:00
myasnikovdaniil dc3929373b test(kubernetes): size the e2e tenant apiserver to the chart default (#3351)
## What this PR does

The `kubernetes-previous` suite failed with `timed out waiting for the
condition on helmreleases/kubernetes-test-previous-version-csi`, and the
reported cause looked like a dependency problem — `csi` waiting on
`vsnap-crd`, `coredns` waiting on `cilium`. It was neither. The cluster
snapshot taken at failure shows the tenant `kube-apiserver` containers
terminated with exit code 137 within about two minutes of each other:
one killed for failing its liveness probe as reclaim stalled it, the
other OOMKilled outright, both against `limits.memory: 512Mi`.

The e2e fixture pinned the tenant control plane to the legacy `small`
preset — 1 CPU and 512Mi — while the chart ships `c1.medium`, 2 CPU and
2Gi, as the default for this field. A two-node tenant cluster running
the full addon set opens enough watches to cross 512Mi once the workers
join, so the apiserver restarts, and every in-flight HelmRelease that is
polling a DaemonSet rollout burns its entire ten-minute budget while the
control plane is unavailable. That is why two unrelated releases, `csi`
and later `ingress-nginx`, failed with the same signature while the
Deployment-based releases that had installed earlier succeeded. The
`DependencyNotReady` lines are aggregated first-occurrence events —
`vsnap-crd` had in fact installed successfully nine minutes before
cilium did.

This is a defect in the fixture rather than in the product: users on
defaults get four times the memory. The default was raised deliberately
in v0.40.4, "to ensure more reliable operation under higher workloads
and prevent resource constraints"; the fixture was left behind on an
unexplained downward override. Setting it to the chart default makes the
test exercise a control plane sized the way a real one is.

No timeout is widened and no retry is added, since either would hide the
cause rather than remove it. The sibling OIDC fixtures keep the smaller
preset, correctly — they run a single replica with no workers, where
this failure mode cannot occur. Suites are executed serially, so the
additional memory applies to one tenant cluster at a time.

What this does not prove is that 2Gi is empirically sufficient; that is
the value the chart itself ships, so if it still proves too small, the
finding belongs to the product default rather than to this fixture.

### Release note

```release-note
NONE
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* Updated the Kubernetes test environment’s control plane resource
allocation to improve reliability and reduce out-of-memory errors and
timeouts.
* **Documentation**
* Added guidance explaining the resource selection and previous failure
conditions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-20 12:26:01 +05:00
Aleksei Sviridkin 44b221d772 test(kubernetes): size the e2e tenant apiserver to the chart default
The tenant Kubernetes e2e pinned controlPlane.apiServer.resourcesPreset to
the legacy "small" preset, capping the apiserver at 1 CPU / 512Mi -- a
quarter of the memory the chart itself ships as the default (c1.medium,
2 CPU / 2Gi).

512Mi does not hold a two-node tenant cluster running the full addon set.
Once the workers join, cilium, coredns, metrics-server, csi, ingress-nginx
and VPA all establish watches at roughly the same moment, and the apiserver
crosses the limit: one replica is killed for failing its liveness probe as
reclaim stalls it, the other is OOMKilled outright. Both replicas restart
within about two minutes of each other, so any tenant HelmRelease that is
mid-install and waiting on a DaemonSet rollout observes 'InProgress' for
its entire timeout and fails, then re-enters a fresh install on remediation.

Which release loses is a matter of timing rather than of the release, which
is why the failure moved between the csi, ingress-nginx and cilium releases
and looked like an unrelated dependency problem each time.

Use the chart default so the test exercises a control plane sized the way a
real one is, instead of one held at a quarter of the shipped default.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-19 20:24:42 +03:00
Aleksei Sviridkin 3a82ed43bc test(etcd): stop losing the metrics probe's output to the attach race
The metrics contract runs a one-shot curl Pod and reads a `code=<status>`
marker back through `kubectl run --attach`. Attach connects only after the
Pod reaches Running, so a container that exits before the connection is up
writes its stdout into the void: the step then sees only kubectl's own
"If you don't see a command prompt, try pressing enter." and reports the
metrics endpoint as broken. curl against a Pod IP finishes in milliseconds,
so losing that race is routine, and `--rm` compounded it by deleting the Pod
the instant it exited, taking the log that still held the real output.

The result was a false negative on a healthy cluster, non-deterministic in
the way that reads as a flake because whether attach wins depends on Pod
startup timing rather than on anything under test.

Drop `--rm`, keep the Pod until its result is in hand, and re-read the log
when the attach stream carried no marker. The recovery cannot mask a real
failure: the marker encodes the outcome, so a recovered `code=503` stays a
failure, and a stream that already carried a marker is never second-guessed.
The pre-run delete stays blocking so the re-read cannot find a previous
incarnation's marker, and the post-run delete is guarded so a transient API
error cannot abort the step with the result in hand but unreported.

Pin `--pod-running-timeout` at 90s rather than leaning on kubectl's 1m
default. curlimages/curl is not otherwise used on these nodes, so the probe
pays a cold pull, and a pull that outruns the budget produces the same
markerless output this change exists to explain.

The resolver is split into a sourced helper so the recovery is unit-tested
without a cluster; the kubectl orchestration around it stays covered by the
e2e run. The image-cache probe carries the same recovery inline for the same
race.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-19 20:16:01 +03:00
Aleksei Sviridkin 1e4a152291 test(mariadb): give first boot the readiness budget the probe grants
The suite allowed 3 minutes for a MariaDB endpoint address to appear,
which was set when a replica had 40 seconds to finish its first boot.
The startup probe now grants 310 seconds, so a healthy install can
legitimately take longer than the gate allows and would be failed while
the probe is still willing to wait.

The endpoint assert is the gate that matters here. The chart hands the
operator a MariaDB custom resource and ships no built-in workload, so
helm has nothing to wait on and the HelmRelease reports Ready as soon as
the manifests are applied, long before a server exists. An endpoint
address, by contrast, appears only once a replica has passed its startup
and then its readiness probe, so that assert is what actually covers
bootstrap.

Raise it to 10m in both the replicated and the single-replica test. One
ready address satisfies the assert, so it has to clear a single startup
budget rather than one per replica, plus the operator reconcile, PVC
bind and image pull that precede it. The HelmRelease ceilings stay at
6m, since nothing about them gates on the server coming up.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-19 01:03:05 +03:00
Andrey KolkovandClaude Fable 5 bb1127c767 test(sdn): drop the serviceexposure chainsaw suite left after the feature removal
Commit 3dbc7d366 removed ExposureClass/ServiceExposure (api/network, the
controller, and the CRDs shipped in cozystack-controller/definitions) in
favor of the native LoadBalancer path, but the chainsaw port that landed
the next day (dd2f89d20) brought the serviceexposure suite along. The
suite creates network.cozystack.io/v1alpha1 fixtures against an API that
no longer exists, so every full-suite run (any PR touching core paths,
and every release cut) fails its two tests with "no matches for
network.cozystack.io/v1alpha1".

Remove the dead suite and its wiring: the suite_to_source case and its
core-platform bats test in select-install (the "-" mechanism stays for
the next core-platform-owned suite), and the README suite list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-17 14:20:56 +04:00
Andrey KolkovandClaude Fable 5 50fb3c87c2 test(postgres): run the backup round-trip e2e in CI under TIA
Ungate postgres-2-backup-roundtrip: instead of an opt-in env flag it now
runs whenever TIA selects the postgres suite (postgres changes, and every
release cut), like the other chainsaw tests. The blocker was network
placement, not capability: the S3 client is the barman-cloud sidecar
inside the Postgres Pod, and the isolated e2e tenant's Cilium egress
allowlist blocks it from tenant-root's seaweedfs — so the Test runs in
tenant-root (same-tenant egress) against the in-cluster S3 endpoint,
trusting its self-signed cert via the copied seaweedfs-system-ca-cert.
This is exactly the recipe validated end to end on a real cluster.

Also map examples/backups/<app>/ edits to that app's suite in
hack/select-e2e.sh (the round-trip executes those scripts as its
harness; previously such edits were silently ignored), with bats
coverage for both the mapped and the no-suite case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-16 21:44:41 +04:00
Andrey KolkovandClaude Opus 4.8 bdc0b564e2 test(postgres): add gated barman-cloud backup/restore e2e round-trip
Port examples/backups/postgres into an automated e2e, following the etcd
backup precedent: the numbered example manifests become a runnable harness
(00-helpers.sh, run-all.sh, cleanup.sh) that a human reads and the test
drives, so the documented flow and the test can never drift.

run-all.sh provisions a Bucket, deploys the source Postgres (writing a
sentinel row), derives the two Secrets the barman-cloud ObjectStore
references from the Bucket's BucketInfo and seaweedfs' CA, submits an
ad-hoc BackupJob and waits for Succeeded, then restores to a copy and
asserts the sentinel round-tripped through S3 — the in-cluster witness
that a plugin backup is both taken and restorable end to end.

A second Test document (postgres-2-backup-roundtrip) in the existing
hack/e2e-chainsaw/postgres suite drives run-all.sh, gated out of CI behind
POSTGRES_E2E_S3_ROUNDTRIP=1 (kind has no reachable, CA-trusted S3 endpoint,
and a tenant Pod cannot cross the Cilium egress policy to tenant-root's
seaweedfs). Placing it in the postgres suite (rather than a new dir) keeps
it triggered by packages/apps/postgres changes under Test Impact Analysis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-16 21:44:41 +04:00
Andrey KolkovandClaude Opus 4.8 604e79437b docs(backups): disclose CNPG-bump + LINSTOR scheduler change; fix stale e2e fixture
Review follow-up (cozystack#3300):

- docs/operations/backup-classes.md: add an operator-facing upgrade note for the
  two platform-wide changes this migration carries — the CloudNativePG operator
  bump to 1.28.1 (with the CRD-skew fix and the Helm-CRD-upgrade caveat) and the
  LINSTOR scheduler admission-webhook removal, which makes data-locality
  scheduling advisory cluster-wide (spec.schedulerName no longer auto-assigned;
  opt in via explicit schedulerName). These affect more than Postgres backups and
  were previously only noted in a chart values.yaml comment.
- hack/e2e-chainsaw/postgres/postgres.yaml: drop stale restic-era backup fields
  (s3Region, s3Bucket, cleanupStrategy, resticPassword) that no longer exist in
  the Postgres backup schema; use current fields (destinationPath, endpointURL,
  retentionPolicy, 6-field schedule).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-16 21:44:41 +04:00
Myasnikov DaniilandClaude Opus 4.8 8af4ed543e test(e2e): address review nits — shellcheck directive + ports-workaround TODO
Non-blocking review notes from #2826:

- Add `# shellcheck shell=bash` to the four sourced `_lib/*.sh` helpers
  (etcd-cleanup, remediation-guard, run-kubernetes, talos-image-cache) so
  shellcheck stops flagging SC2148 (no shell directive) — they are only ever
  `. sourced`, so there is no runtime interpreter to infer from a shebang.
- Add a `# TODO(chainsaw>0.2.15)` marker at the postgres Service asserts
  explaining the `(ports[0].port)`/`(ports[*].port)` projection is a v0.2.15
  number-literal-filter workaround (and that `ports[0]` checks only the first
  port), so it gets reverted on a chainsaw bump; cross-references the same
  workaround in kafka/mariadb/harbor and the e2e-testing.md gotchas section.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-10 17:02:46 +05:00
Myasnikov DaniilandClaude Opus 4.8 ff0d0d77a7 test(e2e): make the talos image cache reachable by tenant workers (#3254)
The in-sandbox Talos image mirror (#3244) is unreachable by the tenant
workers it serves, so the kubernetes-* e2e tests fail deterministically at
the 12m node-join wait instead of relieving the public-factory flake (#3231).

Root cause (from #3254 by @lexfrei, verified against the failing run's
crust-gather snapshot): tenant namespaces run a default-deny Cilium egress
(allow-internal/external-communication + the <tenant>-egress CCNP) that
permits the world entity and the tenant tree but NOT an arbitrary kube-system
Service, so a worker's CDI importer resolves talos-image-cache.kube-system.svc
yet its TCP connect to the ClusterIP is silently dropped and no disk imports.
#3244's gate never caught this because it probed the server over localhost
inside the serve pod, which passes regardless of cross-pod reachability.

Port of #3254 to the Chainsaw layout (hack/e2e-chainsaw/_lib/):
- ship a tightly-scoped CiliumClusterwideNetworkPolicy (importer pods in
  tenant-test -> mirror pod in kube-system), applied at point-of-use once
  Cilium CRDs exist; excluded from the pre-Cilium install apply via yq.
- replace the readiness probe with a throwaway pod in tenant-test labelled
  cdi.kubevirt.io=importer that curls the ClusterIP with a range request and
  must get 206 - the exact egress path a real importer faces; tenants use the
  mirror only when that end-to-end check passes, else fall back to the public
  factory, so it can only help, never make CI worse.
- unit test pinning the manifest split, probe/policy label agreement, the
  strict 206 gate, and the --overrides JSON builder.

Also adds (a2) DataVolume import-stage diagnostics to the node-join failure
block in run-kubernetes.sh (importer logs + a ClusterIP re-probe via
talos_image_cache_diagnose) so the import-stage failure mode is legible.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-09 18:21:07 +05:00
Myasnikov DaniilandClaude Fable 5 1f84d2d49e Merge origin/main into feat/e2e-chainsaw-example (post-#3244)
Fold the in-sandbox Talos image cache work into the Chainsaw layout:
the new shared helper relocates to hack/e2e-chainsaw/_lib/
talos-image-cache.sh and run-kubernetes.sh sources it from there (the
ouroboros DNS-check no-retry rework merges clean). The OIDC bats are
resolved as deleted again; their Chainsaw fixtures deliberately skip
the talos.imageFactoryURL injection — the helper only emits it when
the cache is up, and the zero-worker render-side tests instantiate no
DataVolume imports for it to redirect.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-09 10:38:35 +05:00
Myasnikov DaniilandClaude Fable 5 dea35f7432 Merge origin/main into feat/e2e-chainsaw-example (post-#3233)
Main's #3233 landed mid-reconciliation and touched the two OIDC bats
again: nodeGroups (and storageClass) dropped from the render-side test
CRs — the schema default {} now renders one zero-replica
MachineDeployment via CAPI defaulting, so the tests provision zero
worker DataVolumes (#3231). Resolve the modify/delete as deleted and
mirror the fixture change (and its rationale) into the ported
kubernetes-oidc-system / kubernetes-oidc-customconfig Chainsaw suites.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-08 21:33:08 +05:00
Myasnikov DaniilandClaude Fable 5 1f9555a3c6 test(e2e): resolve outstanding bot review findings
- redis: assert both StatefulSet-templated PVCs Bound (replicas: 2
  provisions two, only ordinal 0 was checked)
- gateway: || true each delete inside the three cleanup() traps — they
  run under set -eu, so one transient delete failure skipped the
  remaining cleanup commands
- bucket, backup(.disabled): guard the trap's kill against an unset
  pf_pid (script can exit before the port-forward starts)

Not addressed on purpose: the sort(ports[*].port) suggestions on
clickhouse/kafka — sort() is among the JMESPath forms chainsaw v0.2.15
(still the latest release) mis-evaluates, which is why the suites use
the ordered (ports[*].port) projection; services keep spec order.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-08 19:13:01 +05:00
Myasnikov DaniilandClaude Fable 5 dd2f89d208 test(sdn): port the securitygroup and serviceexposure suites to Chainsaw
Main added securitygroup.bats (#2922 membership model) and
serviceexposure.bats (#3081 ExposureClass/ServiceExposure) after the
Chainsaw migration branch was cut.

- securitygroup: two Tests — the aggregated-API projection round-trip
  (backing CNP marker label, empty-value membership endpointSelector,
  lineage fromApp + membership fromSG peers, 1:1 rule translation, the
  view hiding storage internals, unmarked-CNP invisibility via an error
  op, delete propagation) and the controller membership-label
  stamp/clear lifecycle. Only the attachments annotation stays a jq
  script (it is a JSON string). Chainsaw auto-cleanup covers every
  fixture even on mid-test failure — the bats had no teardown hook and
  leaked its fixtures.
- serviceexposure: the externalIPs backend resolve/Ready/assignedIPs
  path with the no-MetalLB-pool negative as an error op (ordered after
  resolvedBackend so it cannot false-pass), and the finalizer-removal
  test collapsed onto the delete op itself, which waits for full
  disappearance.
- selection: kubernetes-application now also maps to the two OIDC
  render-side suites (they exercise the same app chart — a gap main's
  bats selection had too); the new dirs enter all_apps discovery
  automatically. Selector unit tests extended and green (13/13 under
  cozytest).
- README: suites list updated.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-08 18:24:24 +05:00
Myasnikov DaniilandClaude Fable 5 cac07db8fc test(kubernetes): port the Phase-1 OIDC render-side suites to Chainsaw
Main added kubernetes-oidc-system.bats and
kubernetes-oidc-customconfig.bats after the Chainsaw migration branch
was cut; without a port the 260 lines of coverage would vanish with
the hack/e2e-apps/ deletion.

Both are light cluster e2e (apply CR -> assert rendered objects; no
Kamaji bringup, minReplicas: 0), exactly Chainsaw's declarative shape:
- System mode: KamajiControlPlane carries --authentication-config +
  the chart-owned volume/mount; the AuthenticationConfiguration Secret
  carries the cozy-realm issuer (regex, script step) + per-cluster
  audience; per-cluster KeycloakClient/KeycloakClientScope land in the
  cozy realm
- CustomConfig: the flag is still wired; the Secret carries the
  tenant-supplied issuer and NOT the cozy realm (fail-loud negation);
  no Keycloak objects and no pre-rendered OIDC kubeconfig Secret,
  expressed as error ops ordered after the positive asserts so an
  unreconciled HR cannot false-pass them

The port also sheds two latent bats bugs: cozytest defines no skip()
(the Keycloak guard would have died with exit 127 had it ever fired)
and the api-resources group probe exits 0 even when the group is
absent — the chart capability-gates Keycloak rendering and CI always
ships the operator, so the asserts are unconditional here.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-08 18:20:20 +05:00
Myasnikov DaniilandClaude Fable 5 6c76a462a0 test(e2e): fold remaining post-branch bats drift into the Chainsaw suites
- qdrant: add the post-delete PVC-reclaim guard (#3059) — delete the CR
  and poll the ordinal-anchored data PVC to absence via the error op
  (retries transient API errors internally), with a PVC/jobs/pods dump
  on failure; Chainsaw auto-cleanup alone would never notice a leaked
  StatefulSet-templated PVC
- kuberture: make the split-horizon negative log assertions fail loud —
  under set -e a pipeline beginning with ! suppresses errexit, so both
  leak checks silently passed (the same vacuous-negation bug main fixed
  in 12fe3256f)
- foundationdb: retry the one-shot generations.reconciled read for up
  to 2m — the operator stamps it after the post-bounce uptime gate, so
  it can trail the fdbcli-derived health fields the previous assert
  converged on (a79f8009f)
- kubernetes-latest: add the version-independent SC-fallback-default
  Test (#2872 B1) calling verify_storageclass_fallback_default from the
  shared lib; a separate Test doc so it runs regardless of the heavy
  bringup Test's outcome, safe under the suite-wide parallel: 1 while
  it temporarily mutates cluster-scoped StorageClasses

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-08 18:17:19 +05:00
Myasnikov DaniilandClaude Fable 5 d6ac62a6d9 test(bucket,harbor): fail fast on frozen BucketClaims; enforce readonly denial
Fold main's post-branch bucket/harbor bats drift into the Chainsaw
suites:

- BucketClaim bucketReady budgets tightened to 2m (bucket had the 5m
  default, harbor a stale 10m with the reversed 'can lag on a loaded
  runner' rationale): the objectstorage-controller requeues the claim
  until the backend Bucket is ready (0f1cc548b), so readiness converges
  in tens of seconds and a claim not ready after 2m is frozen — the
  tight bound surfaces the propagation regression instead of masking
  it (63d14e748)
- the bucket viewer-upload soft-xfail is promoted back to a hard
  failure: accessPolicy=readonly enforcement landed with
  seaweedfs-cosi-driver v0.3.1 (#1921, eab9b034f), so a successful
  readonly upload is now a regression, exactly what the suite's own
  TODO called for
- both catch blocks gain the backend view (cluster-scoped Buckets +
  cozy-objectstorage-controller pods) where a frozen claim actually
  surfaces

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-08 18:15:56 +05:00
Myasnikov DaniilandClaude Fable 5 21e21a9234 test(kafka): raise fixture presets off the 128Mi OOM threshold
Main raised kafka/zookeeper resourcesPreset nano -> c1.small
(de9c81ce6, #3052) after the Chainsaw port was cut; the fixture still
pinned both to nano.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-08 18:14:04 +05:00
Myasnikov DaniilandClaude Fable 5 7b557db59f test(mariadb): port the single-replica webhook-regression guard to Chainsaw
Main's mariadb.bats gained "Create single-replica MariaDB" after the
Chainsaw port was cut: replication is guarded on replicas>1
(c7e8f4d97), so replicas=1 must render a CR the operator webhook
admits and provision the bare <name> Service without the
-primary/-secondary split. Mirrors the bats assertions (svc :3306 +
endpoints, StatefulSet replicas=1, metrics svc/endpoints/deployment)
and dumps the rendered k8s.mariadb.com CR in catch so a webhook
rejection is legible.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-08 18:14:04 +05:00
Myasnikov DaniilandClaude Fable 5 c8e775d2d0 test(gateway): port the 12 ingress-hostname-policy cases to Chainsaw
Main added cozystack-ingress-hostname-policy (d89ad7069) with 12 bats
cases after the Chainsaw port was cut; this appends them as one Test.

- VAP/binding installation asserted declaratively (binding must carry
  Deny)
- rejects and accepts grouped by derivation needs: apex-derived rejects
  (foreign-apex host, dot-boundary prefix-adjacent, foreign tls host,
  wildcard matching the root apex), static catch-all rejects (hostless
  rule, empty-string host, defaultBackend), own-apex accepts (in-apex
  rule+tls, own-apex wildcard), and static external-domain accepts as a
  declarative apply with auto-cleanup
- hostnames derived at runtime: the tenant apex from the test
  namespace's namespace.cozystack.io/host label (suite convention) and
  the platform ROOT apex from the same label on tenant-root, replacing
  the bats' hardcoded test.example.org/example.org; evil${apex}
  preserves the dot-boundary semantics of eviltest.example.org, and the
  external-domain probes use the RFC 2606 .invalid TLD so they can
  never collide with a real cluster apex

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-08 18:12:42 +05:00
Myasnikov DaniilandClaude Fable 5 ece64ee0a9 test(etcd): re-port the Chainsaw suite to the v1alpha2 operator contracts
The suite was ported from the pre-#2859 etcd.bats and asserted
etcd.aenix.io/v1alpha1 objects that no longer exist on main — every
Test hard-failed at the first assert on an unknown GVK.

- drop the etcd-empty-backup and etcd-backup-schedule scenarios with
  their fixtures: the chart no longer renders EtcdBackupSchedule or
  etcd-s3-creds (backups moved to the generic backups API), and
  legacy-values tolerance is pinned by helm-unittest, not e2e
- rewrite the remaining scenario into etcd-1-contracts, mirroring
  main's contracts test: gate on status.readyMembers=3 (Available=True
  flips at seed-member discovery and races replica-count contracts),
  assert the operator-owned member Pod shape (strict selector,
  container name, named metrics port), WorkloadMonitor
  operational/availableReplicas, and script the three contracts that
  need imperative probes: the /scale subresource, the plaintext-HTTP
  metrics probe against the Pod IP, and the on-demand defrag Job
- add etcd-2-backup-roundtrip driving examples/backups/etcd/run-all.sh,
  gated on ETCD_E2E_S3_ROUNDTRIP=1 exactly like main (kind CI has no
  publicly-trusted S3 endpoint the snapshot agent could verify)
- upgrade catch blocks to the v1alpha2 GVKs plus EtcdMember/
  EtcdSnapshot/BackupJob describes and the snapshot-Job Pod logs
- strengthen _lib/etcd-cleanup.sh with main's etcd_drain deltas: probe
  the Etcd CR itself (it can linger Terminating and block the next
  singleton apply) and use the err-sentinel pattern so a transient API
  error is never misread as "deleted"; add etcd_backup_cleanup wrapping
  examples/backups/etcd/cleanup.sh for the round-trip's teardown

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-08 18:10:20 +05:00