## 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, sincefd14126be, refuses to render one that does not start with `kubernetes-nodes-<spec.cluster>-` (`packages/apps/kubernetes-nodes/templates/_helpers.tpl`). The fixture, added later in7500a9fba, 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 -->
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 waitpair that appeared ~100 times collapses into a singleassertthat 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 —
catchblocks dump events,describe, and pod logs on any failure (the BATS suite only did this inharbor.bats, by hand). - Automatic cleanup — resources Chainsaw applied are deleted (and waited on) in reverse order;
finallycovers operator-created leftovers. - CI-ready output — a JUnit
chainsaw-report.xmlis 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/unsealviakubectl exec. - kuberture —
jqon 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
LiveMigratablecondition. - gateway — admission-rejection cases that need
kubectl --asimpersonation or error-message greps; the tenant apex is read from the namespacenamespace.cozystack.io/hostlabel at runtime so they pass on any host. - kubernetes-latest/previous — wrap
_lib/run-kubernetes.sh(Kamaji bringup, port-forward, LB/NFS/ouroboros). The scriptcds to the repo root (the test dir ishack/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:Thestatus: (conditions[?type == 'Ready']): - status: "True"(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. errorwith a bare resource ref (name only) asserts the resource does not exist (used for "PVC reclaimed", "no backup schedule", "child has no Gateway HR").scriptruns in the test directory with$NAMESPACEset 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.