Files
cozystack/hack/e2e-chainsaw
Aleksei Sviridkin c1071dab9b feat(kubernetes): let a node group log the guest serial console (#3637)
## What this PR does

Both tenant Kubernetes e2e suites keep missing the node-Ready budget
with zero Nodes registered, and the time goes inside the guest, between
the worker VMI reaching Running and Talos asking for its apid
certificate. Nothing collected today reaches that window. The in-guest
capture the harness already has drives `talosctl`, so it needs the apid
the worker never got to, and the management side has neither a Node nor
a certificate request to read. Four attempts at this failure have been
designed from outside the guest; the most recent raised the budget from
12m to 18m and the same fingerprint came back at the new deadline.

KubeVirt can stream a VM's guest serial console into a
`guest-console-log` container beside virt-launcher, readable with
`kubectl logs` and needing nothing from inside the guest — no talosctl,
no client certificate, no helper Pod, no reachable apid. That is the one
property this failure class demands, so this turns it on for the suite's
worker node group and collects it into the diagnostic bundle when
node-join fails, ahead of the `talosctl` capture that cannot describe
the same runs.

Reads start at the beginning of the stream rather than tailing it: a
guest that repaints its console would push the boot output out of any
`--tail` window, while `--limit-bytes` truncates from the far end
instead. Every read is bounded, and the walk stops at a cap that records
both counts, so the collector cannot become an unbounded term in a
failure path that already runs several others under one deadline.

A read that returns no console is not filed as a quiet guest. Three
outcomes get three labels, because only one of them says anything about
the guest: the read failed and returned nothing, the read succeeded and
returned nothing, or it broke off part way. Each read's stderr is kept
beside the capture rather than inside it — kubectl's own `container
guest-console-log is not valid for pod` would otherwise fill the file,
and a console that never started would read as one that had spoken. That
case is the headline one here, so the Pod state and events that explain
it are collected alongside, once for the whole selector.

The switch is a new opt-in `logSerialConsole` on a node group, off by
default. It renders nothing unless set to true, including when set to
false, because the `KubevirtMachineTemplate` is content-hash named and a
byte emitted for a group that did not ask for it renames the template
and rolls every live worker VM on upgrade. I verified that by rendering
the base tree and this branch: absent and explicit-false produce a
byte-identical template name on both charts. Worker VMs come from two
charts, `kubernetes` and `kubernetes-nodes`; both carry the field, and
the golden-parity check between them gains a case that keeps their
output identical.

The passing path checks separately that the container actually attached,
and runs that check last, after everything the suite exists to prove — a
debugging aid should not preempt the assertions that cover the cluster.
Without it the suite would first learn the setting was inert in the one
run that needed the console, when it can no longer be recovered.

Relates to #3513.

### The risk this carries, stated up front

The platform turns serial console logging off cluster-wide in the
KubeVirt CR. That setting is a mitigation added in February 2026 for
kubevirt/kubevirt#15989, where the `guest-console-log` container kept
virt-launcher from starting and left VMs in `PodInitializing`. A per-VM
`logSerialConsole` takes precedence over it —
`isSerialConsoleLogEnabled` reads the VMI field first and only then
falls back to the cluster setting — which is what makes this work, and
also makes this the first thing on the platform to go through that
mitigation. The mitigation was written against KubeVirt v1.6.x; the
platform now ships v1.8.4, and the upstream issue is still open with no
reproduction reported either way on the newer version.

So the first run of these suites is also the experiment, and it doubles
as a revalidation of that February mitigation on the version actually
shipping, which nobody has done. The two outcomes are distinguishable
rather than ambiguous: kubevirt/kubevirt#15989 shows up as virt-launcher
wedged in `PodInitializing` with the VMI never reaching Running, while
the failure this instruments shows the VMI Running and zero certificate
requests. A red run with the first signature is an answer, not noise —
and the collector is built so that it says which one happened instead of
reporting silence. The blast radius is small: the product field is
absent by default and only the e2e node group sets it, so backing this
out is one line.

The revert criterion, stated before the run rather than after it. If
worker virt-launcher Pods sit in `Init` on `guest-console-log` and no
Node registers, kubevirt/kubevirt#15989 still reproduces on v1.8.4: drop
the `logSerialConsole: true` line from the e2e node group, keep the
chart field, and the upstream issue gains the reproduction on a current
version that its thread does not have. If the Pods reach Running and the
console logs carry boot output, the mitigation is a candidate for
removal platform-wide, which is its own change. Either way the run
answers something that was open before it.

Dropping `virtualMachineOptions.disableSerialConsoleLog` outright was
the alternative. It is a smaller diff and needs no values key, but it
exposes every VM on the platform to a race nobody has re-tested, instead
of one node group in one test. That is its own revalidation exercise and
belongs in its own change, on evidence this one produces.

### Verification

`make unit-tests` passes. The new bats suite runs green under both
`bash` and a `dash` `/bin/sh`, which matters because `hack/cozytest.sh`
is `#!/bin/sh` and an earlier revision of this branch carried a
bash-only construct that failed silently there. Every test in it is
killed by a specific mutation of the code it covers. Both charts'
helm-unittest suites pass, the golden-parity script reports
byte-identical output across all four cases including the new one, and
`make generate` is idempotent at the repo root.

Reviewed by Claude across several rounds and by an independent second
reader; the review loop found and fixed real defects in this branch,
including a green-path check that had become unable to report the one
thing it existed for.

### Screenshots

Not a UI change.

### Downstream repositories

- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [x]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
https://github.com/cozystack/terraform-provider-cozystack/issues/26
- [ ]
[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
feat(kubernetes): worker node groups accept `logSerialConsole`, which streams a worker VM's guest serial console into a `guest-console-log` container readable with `kubectl logs`. Off by default. It is the only way to observe a worker that stalls during boot before Talos apid answers, when no Node and no certificate request exist to diagnose from. Enabling it rolls the node group and overrides the platform's cluster-wide `disableSerialConsoleLog`.
```


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

* **New Features**
* Added an optional `logSerialConsole` setting for Kubernetes worker
node groups.
* Streams worker VM guest serial-console output to a dedicated log
container when enabled.
* Configuration is disabled by default and available through Kubernetes
application settings.
* **Bug Fixes**
* Improved worker failure diagnostics with bounded serial-console output
and pod-state information.
* Validates that enabled worker pools attach the expected console-log
container.
* **Documentation**
* Documented configuration behavior, rollout effects, defaults, and
timeout considerations.
* **Tests**
* Added coverage for rendering, attachment validation, capture limits,
and failure scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-08 10:47:00 +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 chainsaw test hack/e2e-chainsaw/ after make install-cozystack; cluster bootstrap (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, qdrant, clickhouse, kafka, etcd, openbao, harbor, foundationdb, external-dns, kuberture, vminstance, gateway, kubernetes-latest, kubernetes-previous, kubernetes-oidc-system, kubernetes-oidc-customconfig, securitygroup.

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.
  • 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 additionally needs mc and nc on the host; kuberture/openbao/harbor 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 chainsaw test hack/e2e-chainsaw/ inside the e2e sandbox) and uploads chainsaw-report.xml. The sandbox image (packages/core/testing/images/e2e-sandbox/Dockerfile) ships the chainsaw binary alongside kubectl/helm/mc/jq/nc.