Files
cozystack/hack/e2e-chainsaw
Aleksei Sviridkin 040ae377ae test(kubernetes): name the Proxmox fixture's node pool <cluster>-<pool> (#5000)
## What this PR does

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

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

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

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

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

### Testing

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

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

### Found on the way, not changed here

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

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

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

### Screenshots

Not a UI change.

### Downstream repositories

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

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

### Release note

```release-note
NONE
```


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

## Summary by CodeRabbit

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-29 11:19:21 +03:00
..

Chainsaw E2E tests

The Cozystack E2E app suite, written for Kyverno Chainsaw — a declarative, Kubernetes-native E2E test framework (CNCF, part of the Kyverno project; successor to KUTTL).

This directory replaces the per-app BATS suite that used to live in hack/e2e-apps/*.bats. The CI e2e job runs every suite under hack/e2e-chainsaw/ through hack/e2e-chainsaw-run.sh after make install-cozystack; cluster bootstrap (hack/e2e-prepare-cluster-container.bats, hack/e2e-install-cozystack.bats) and the OpenAPI checks (hack/e2e-test-openapi.bats) remain BATS.

Layout

hack/e2e-chainsaw/
├── .chainsaw.yaml          # global Configuration (namespace, timeouts, JUnit report)
├── _lib/                   # shell reused by the script-heavy suites
│   ├── run-kubernetes.sh   #   tenant Kubernetes bringup + LB/NFS/ouroboros checks
│   └── remediation-guard.sh
├── <app>/
│   ├── chainsaw-test.yaml  # one or more Test definitions
│   └── <app>.yaml          # the manifests the test applies
└── ...

Suites: postgres, bucket, mariadb, mongodb, redis, valkey, qdrant, clickhouse, kafka, kafka-metadata, rabbitmq, etcd, openbao, harbor, foundationdb, external-dns, kuberture, vminstance, gateway, opensearch, monitoring, kubernetes-latest, kubernetes-previous, securitygroup, seaweedfs, computeplane.

What Chainsaw buys over the BATS suite

  • Readable tests — a list of resources and expected states, not shell control flow. The timeout N sh -ec "until kubectl get ..." + kubectl wait pair that appeared ~100 times collapses into a single assert that polls existence and state together.
  • Distinguishable failure modes — "object never created", "created but never ready", and "wrong status" all surface as a structured diff instead of an opaque shell timeout.
  • Automatic diagnostics — catch blocks dump events, describe, and pod logs on any failure (the BATS suite only did this in harbor.bats, by hand).
  • Automatic cleanup — resources Chainsaw applied are deleted (and waited on) in reverse order; finally covers operator-created leftovers.
  • CI-ready output — a JUnit chainsaw-report.xml is emitted for the test summary.

Test anatomy

Each suite is a directory with a chainsaw-test.yaml (one or more Test docs) plus the manifests it applies. A step has try (operations: apply, assert, error, script, delete, …), optional catch (diagnostics on failure), and optional finally (teardown regardless of outcome).

Declarative suites assert on status.conditions and concrete fields. Inherently imperative suites keep their logic in script steps:

  • openbao — bao operator init/unseal via kubectl exec.
  • kuberture — jq on Service annotations + external-dns split-horizon log assertions.
  • vminstance — a server-side dry-run eviction of the VM's launcher pod, checked against the VMI's LiveMigratable condition.
  • gateway — admission-rejection cases that need kubectl --as impersonation or error-message greps; the tenant apex is read from the namespace namespace.cozystack.io/host label at runtime so they pass on any host.
  • kubernetes-latest/previous — wrap _lib/run-kubernetes.sh (Kamaji bringup, port-forward, LB/NFS/ouroboros). The script cds to the repo root (the test dir is hack/e2e-chainsaw/<suite>, so repo root is ../../..).

Conventions / gotchas (Chainsaw v0.2.15)

  • Condition asserts use the filter-as-list form, not the documented [0] index:
    status:
      (conditions[?type == 'Ready']):
      - status: "True"
    
    The (conditions[?type == 'Ready'])[0]: {status: "True"} idiom fails with "field not found in the input object".
  • A reading of a failure diff: the "actual" side is a projection onto the expected fields — status: {} means the JMESPath expression failed, not that status is empty. Read the error line above the diff first.
  • error with a bare resource ref (name only) asserts the resource does not exist (used for "PVC reclaimed", "no backup schedule", "child has no Gateway HR").
  • script runs in the test directory with $NAMESPACE set to the config namespace (tenant-test).

Running locally

Requires a cluster with Cozystack installed and a tenant-test namespace (the environment hack/e2e-install-cozystack.bats produces). bucket and seaweedfs additionally need mc, nc, and jq on the host; kuberture/openbao/foundationdb/securitygroup/computeplane/kubernetes-latest/kubernetes-previous/vminstance need jq.

# install chainsaw
go install github.com/kyverno/chainsaw@latest   # or: brew install kyverno/chainsaw/chainsaw

# run all suites (config picked up from .chainsaw.yaml)
cd hack/e2e-chainsaw
chainsaw test

# run a single suite
chainsaw test postgres/

kubernetes-latest/kubernetes-previous each bring up a full tenant Kubernetes cluster (~25 min, real worker nodes) and vminstance boots a nested VM — expect a long run for the full set.

CI integration

The e2e job in .github/workflows/pull-requests.yaml runs make -C packages/core/testing test-chainsaw (which execs hack/e2e-chainsaw-run.sh inside the e2e sandbox) and uploads chainsaw-report.xml. The runner calls chainsaw test once per suite directory, fails a suite whose pods outlive its cleanup by more than the default timeouts.cleanup (LEAKED-PODS suite=<dir> in the log; see docs/agents/e2e-testing.md §8 and _lib/leak-allowlist.txt for known, filed leaks), and merges the per-suite reports into that one file. A local chainsaw test as shown above runs the same tests without the leak check. The sandbox image (packages/core/testing/images/e2e-sandbox/Dockerfile) ships the chainsaw binary alongside kubectl/helm/mc/jq/nc, built from source with a patch that keeps an assert polling through transient API errors such as an etcd leader change; a released chainsaw run locally fails the assert on the first such error instead.