Files
cozystack/hack/e2e-chainsaw/postgres/chainsaw-test.yaml
Myasnikov Daniil 186547d835 test(e2e): run the merge-gating lanes on Talos containers
The srv1-srv3 nodes of both lanes that gate a merge -- the same-repo one
in pull-requests.yaml and the fork one in e2e-fork.yaml -- run as Talos
containers instead of QEMU guests, so a tenant worker sits at L2 rather
than L3. Measured in CI on commits where both substrates ran the same
tree: whole-job wall clock down 24-49 minutes, essentially all of it in
Chainsaw, while the two tenant-Kubernetes suites go from 1143-2040s to
466-809s and stop depending on the runner's vgif flag.

The node-join soft-red gate goes with the substrate, which is what it
was for. It existed because nested virtualisation on the shared runners
degraded far enough that a worker registering in minutes elsewhere could
miss any deadline this test could afford; #3513 established that the
discriminator was the host's kvm_amd vgif flag, and removing a nesting
level removes what it acted on. So hack/e2e-node-join-soft-red.sh, the
SOFT-RED-node-join.txt marker and the soft_red job outputs are gone from
all four workflows, and a missed deadline is an ordinary red again. The
::warning annotation survives, because the reason a run went red should
be legible without opening the log, and so does the 124-versus-everything
-else test, because "the workers were slow" and "the wait never ran"
send a reader to different places.

Done by swapping the substrate inside the existing e2e jobs rather than
by promoting a separate container job. Those jobs carry more than a
substrate -- the GitHub App token, the report-overrun guard, the images
list, the SSH breakpoint -- and promoting a job that never had them
would have dropped four features without saying so.

Three properties of container mode drive the design, and each fails
quietly rather than loudly. machine.kernel.modules is a silent no-op --
kernel_module_spec.go returns early on ModeContainer with no error and
no event -- so the host loads openvswitch and zfs before the nodes start
and the workflow step asserts both rather than trusting them; a missing
module surfaces an hour later looking like a CNI or a storage
regression. Docker gives a privileged container its own tmpfs /dev
seeded once at creation, so /dev/kvm works but ZFS zvols created later
never appear and linstor-csi fails ControllerPublishVolume; the compose
file binds the host devtmpfs, which is also why talosctl cluster create
docker can never carry LINSTOR. And there is no ARP VIP, so
192.168.123.11 stands in for the QEMU lane's .10.

The QEMU machinery stays in the tree and keeps running: nightly.yaml and
e2e-tag.yaml still call `make prepare-env`, so DRBD, the replicated
StorageClass, its Immediate binding mode, the tenant StorageClass
propagation that only applies to remotely-accessible classes, and the
Cozystack Talos node image with its extensions all keep nightly and
release-candidate coverage. None of them is exercised per pull request
any more. That is the trade.

Nothing on the PR path builds or downloads a nocloud disk now, so
build-talos drops the talos-nocloud step and the talos-image artifact.
build-talos itself stays: finalize consumes its digest fragment.

One property this does not fix, recorded beside the runner class because
it will be read off a red run eventually: a container node reports the
HOST's CPU and memory as capacity, and hack/e2e-container-up.sh counters
that with kubelet systemReserved. That corrects the scheduler and not
what a pod reads from /proc, so a workload sizing itself from
/proc/cpuinfo or /proc/meminfo is configured for the whole runner.

Also carried here because the same files carry them: the ghcr.io
pull-through mirror removal (see #4007 -- whichever lands first makes the
other a no-op), the authoritative HelmRelease readiness gate, the
backup-credential preflight, the prepull guard against an image-less
container fragment, and folding the two standalone OIDC suites into the
latest-version tenant cluster.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-09-05 13:19:17 +05:00

345 lines
15 KiB
YAML

# Chainsaw port of hack/e2e-apps/postgres.bats ("Create DB PostgreSQL").
#
# Every `timeout N sh -ec "until kubectl get ..."` + `kubectl wait` pair from
# the BATS test collapses into a single assert: chainsaw polls until the
# resource exists AND matches, or the timeout expires. Failure output is a
# structured diff of the assertion tree against the live object.
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
name: postgres
spec:
# Diagnostics collected automatically when any step fails. The BATS suite
# only does this in harbor.bats, by hand.
catch:
- events: {}
- describe:
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
name: postgres-test
- podLogs:
selector: cnpg.io/cluster=postgres-test
tail: 50
steps:
- name: create-postgres
try:
- apply:
file: postgres.yaml
- name: wait-helmrelease-ready
try:
- assert:
timeout: 6m
resource:
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: postgres-test
status:
# JMESPath filter: condition order in the array does not matter
(conditions[?type == 'Ready']):
- status: "True"
- name: wait-init-job-complete
try:
- assert:
timeout: 2m
resource:
apiVersion: batch/v1
kind: Job
metadata:
name: postgres-test-init-job
status:
(conditions[?type == 'Complete']):
- status: "True"
- name: verify-services-and-endpoints
try:
# Replaces three `until kubectl get svc ... | grep -q 5432` loops
# TODO(chainsaw>0.2.15): the (ports[0].port) / (ports[*].port) projection form in
# these Service asserts is a v0.2.15 workaround for mis-evaluated number-literal
# filters (ports[?port==`5432`]); (ports[0]) also checks only the first port.
# Restore the filter form on a chainsaw bump. Same workaround lives in
# kafka/mariadb/harbor — see "Chainsaw v0.2.15 gotchas" in docs/agents/e2e-testing.md.
- assert:
resource:
apiVersion: v1
kind: Service
metadata:
name: postgres-test-r
spec:
(ports[0].port): 5432
- assert:
resource:
apiVersion: v1
kind: Service
metadata:
name: postgres-test-ro
spec:
(ports[0].port): 5432
- assert:
resource:
apiVersion: v1
kind: Service
metadata:
name: postgres-test-rw
spec:
(ports[0].port): 5432
# Replaces `until kubectl get endpoints ... | grep -q '[0-9]'` loops.
# The read-only endpoint is skipped, same as in the BATS test.
- assert:
timeout: 3m
resource:
apiVersion: v1
kind: Endpoints
metadata:
name: postgres-test-r
(subsets[0].addresses[0].ip != null): true
- assert:
timeout: 3m
resource:
apiVersion: v1
kind: Endpoints
metadata:
name: postgres-test-rw
(subsets[0].addresses[0].ip != null): true
# Functional probe — the BATS test never actually talks to the database.
- script:
timeout: 2m
content: |
set -eu
pod=$(kubectl -n "$NAMESPACE" get pod \
-l cnpg.io/cluster=postgres-test,cnpg.io/instanceRole=primary \
-o name | head -n1)
kubectl -n "$NAMESPACE" exec "$pod" -c postgres -- \
psql -U postgres -tAc 'SELECT 1'
check:
($error == null): true
(contains($stdout, '1')): true
# The init-job is operator-created (not applied by chainsaw), so it is
# not part of automatic cleanup — delete it explicitly, as BATS does.
finally:
- delete:
ref:
apiVersion: batch/v1
kind: Job
name: postgres-test-init-job
# The trust anchor a client verifies against, checked against the live
# operator rather than against our own beliefs about it.
#
# This is the only independent witness the CA-extraction feature has. Its unit
# tests build CNPG's CA Secret from a fixture that encodes the same assumption
# the shipped ApplicationDefinition encodes ("{{ .release }}-ca", key ca.crt),
# so the two agree by construction and cannot catch each other being wrong.
# Only a real CNPG can say whether that name and key are right, and only a real
# apiserver can say whether the lineage webhook actually promotes the
# projection to a tenant-readable object.
- name: verify-tenant-ca-projection
try:
# Postgres never opts out of TLS: CNPG mints its CA regardless of the
# chart's tls flag, so the projection is expected on this plain fixture.
#
# The labels are the assertion that matters. tenant-ca is stamped by the
# controller; tenantresource is stamped by the LINEAGE WEBHOOK, computed
# from the ApplicationDefinition's spec.secrets — the new matchLabels
# include. It is "true" only if the whole chain held: definition selects the
# label, webhook admitted the object, verdict came out positive. Nothing
# short of a live cluster proves that link.
- assert:
timeout: 2m
resource:
apiVersion: v1
kind: Secret
metadata:
name: postgres-test.tenant-ca
labels:
internal.cozystack.io/tenant-ca: "true"
internal.cozystack.io/tenantresource: "true"
type: Opaque
# The projection must be the CA and ONLY the CA. Everything below is
# deterministic once the assert above has passed, so it runs without retry.
- script:
content: |
set -eu
# Exactly one key, and it is ca.crt. Any second key is a leak in the
# making: the source this was lifted from carries a private key.
keys=$(kubectl -n "$NAMESPACE" get secret postgres-test.tenant-ca \
-o go-template='{{range $k, $v := .data}}{{$k}}{{"\n"}}{{end}}' | sort | tr '\n' ' ')
if [ "$keys" != "ca.crt " ]; then
echo "projection must hold exactly ca.crt, holds: $keys" >&2
exit 1
fi
# It lifted the right certificate: the projection's ca.crt is CNPG's
# ca.crt. This is what pins "{{ .release }}-ca" to reality — if CNPG ever
# renames its CA Secret, the controller finds nothing and this fails.
#
# Compare the parsed certificates, not the raw PEM. The controller
# re-encodes ca.crt from the DER it parsed (certificateChainPEM), so a PEM
# header line or a column-width change at the source would redden a
# byte-for-byte comparison of a correct projection. The certificate
# fingerprint is identity-stable across any re-encoding.
src=$(kubectl -n "$NAMESPACE" get secret postgres-test-ca -o jsonpath='{.data.ca\.crt}' | base64 -d)
proj=$(kubectl -n "$NAMESPACE" get secret postgres-test.tenant-ca -o jsonpath='{.data.ca\.crt}' | base64 -d)
if [ -z "$src" ]; then
echo "CNPG's postgres-test-ca has no ca.crt — the declared source name or key is wrong" >&2
exit 1
fi
src_fp=$(echo "$src" | openssl x509 -noout -fingerprint -sha256)
proj_fp=$(echo "$proj" | openssl x509 -noout -fingerprint -sha256)
if [ "$src_fp" != "$proj_fp" ]; then
echo "projection does not carry CNPG's CA certificate" >&2
exit 1
fi
# And the point of the whole exercise: the source carries the CA
# PRIVATE KEY, the projection must not. Asserting the source really
# does hold ca.key keeps this honest — if CNPG ever stopped storing it
# there, this check would otherwise pass while proving nothing.
if ! kubectl -n "$NAMESPACE" get secret postgres-test-ca \
-o go-template='{{if .data}}{{range $k, $v := .data}}{{$k}}{{"\n"}}{{end}}{{end}}' | grep -qx 'ca.key'; then
echo "CNPG's CA Secret no longer carries ca.key — this check has stopped proving anything" >&2
exit 1
fi
if kubectl -n "$NAMESPACE" get secret postgres-test.tenant-ca \
-o go-template='{{if .data}}{{range $k, $v := .data}}{{$k}}{{"\n"}}{{end}}{{end}}' | grep -qx 'ca.key'; then
echo "projection leaked the CA private key" >&2
exit 1
fi
# Belt and braces on the bytes themselves: no PEM key header of any
# flavour survived into the tenant-readable object.
if kubectl -n "$NAMESPACE" get secret postgres-test.tenant-ca \
-o jsonpath='{.data.ca\.crt}' | base64 -d | grep -qi 'BEGIN .*PRIVATE KEY'; then
echo "projection carries PEM private key material" >&2
exit 1
fi
# The delivery path the README actually tells a tenant to use, run as
# the command the README prints. Everything above reads the raw Secret,
# which only an admin can do — it proves the projection's SHAPE and
# nothing about whether a tenant can reach it.
#
# core.cozystack.io/tenantsecrets serves only objects labelled
# tenantresource=true, and that label is the lineage webhook's verdict,
# computed from this app's ApplicationDefinition selectors. So this
# query 404s unless the whole chain held: the definition selects
# tenant-ca, the webhook admitted the projection, and the verdict came
# out positive. It is the one assertion that exercises the registry
# rather than inspecting its input.
anchor=$(kubectl -n "$NAMESPACE" get tenantsecret postgres-test.tenant-ca \
-o jsonpath='{.data.ca\.crt}' | base64 -d)
if ! echo "$anchor" | grep -q 'BEGIN CERTIFICATE'; then
echo "the trust anchor is not served through core.cozystack.io/tenantsecrets" >&2
exit 1
fi
# And the negative that gives the positive its meaning: the registry
# must NOT serve CNPG's key-bearing CA. It carries no tenant-ca label,
# so the definition does not select it and the webhook marks it
# tenantresource=false. If this ever starts resolving, the label path
# has stopped gating on the platform's verdict.
if kubectl -n "$NAMESPACE" get tenantsecret postgres-test-ca >/dev/null 2>&1; then
echo "CNPG's key-bearing CA is being served as a tenant secret" >&2
exit 1
fi
check:
($error == null): true
# The Postgres CR itself was applied by chainsaw and is deleted (and waited
# on) automatically during cleanup — no explicit teardown step needed.
---
# Full barman-cloud backup -> restore round-trips, driving the validated
# example scripts (examples/backups/postgres) as the harness so the test and
# the documented flow can never drift. run-all.sh runs Bucket -> source
# Postgres + sentinel row -> CNPG strategy + BackupClass -> ad-hoc BackupJob
# (waits Succeeded) -> empty target Postgres + to-copy RestoreJob (waits
# Succeeded, asserts the sentinel round-tripped through S3) -> a point-in-time
# restore between two marker writes (step 45, asserts the pre-marker survived
# and the post-marker did not) -> a negative unreachable-target restore
# (step 46, asserts it fails fast with reason RecoveryTargetUnreachable rather
# than hanging to the deadline). The in-cluster witness that a barman-cloud
# plugin backup is taken, restorable, point-in-time-recoverable, and that a
# bad target is rejected quickly.
#
# Runs in tenant-root, not the default tenant-test: the S3 client here is the
# barman-cloud sidecar INSIDE the Postgres Pod, and the e2e tenant is created
# isolated (hack/e2e-install-cozystack.bats) — its Cilium egress allowlist
# blocks a tenant-test Pod from reaching tenant-root's seaweedfs, while a
# tenant-root Pod reaches it over same-tenant egress. The harness targets the
# in-cluster endpoint (S3_ENDPOINT below) instead of the BucketInfo external
# ingress URL, which in CI is an unroutable placeholder, and trusts its
# self-signed cert by copying the seaweedfs CA secret (seaweedfs-ca-cert,
# auto-discovered from the cert-manager Certificate) into the per-app
# endpointCA Secret. (The bucket suite's mc checks lean on the same
# tenant-root seaweedfs, just from the test runner via port-forward.)
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
name: postgres-2-backup-roundtrip
spec:
namespace: tenant-root
skipDelete: true
catch:
- events: {}
- describe:
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
name: postgres-pg-src
- describe:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
name: postgres-pg-src
- describe:
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
- describe:
apiVersion: backups.cozystack.io/v1alpha1
kind: BackupJob
- describe:
apiVersion: backups.cozystack.io/v1alpha1
kind: RestoreJob
# The barman-cloud backup/WAL work runs in the cnpg Cluster Pods' sidecar; a
# failed upload or TLS handshake leaves its error only in that Pod's log.
- podLogs:
selector: cnpg.io/cluster=postgres-pg-src
tail: 200
steps:
- name: run
try:
# The 40m budget is the whole documented flow: bucket provisioning, a
# 2-replica source Postgres, the plugin base backup to S3, an empty target
# app, and THREE sequential purge + re-bootstrap restores against it - the
# latest-point to-copy restore, the point-in-time restore (step 45), and
# the negative unreachable-target restore (step 46, which sets a short
# restoreTimeoutSeconds and must fail at that deadline with reason
# RecoveryTargetUnreachable), and the in-place restore (step 50, the
# regression guard for the archive-vs-recovery serverName collision)
# (convention 6: justified in-line). The old 25m budget predated the PITR
# phases; four restores plus the negative phase need the larger window.
# run-all.sh fails fast on terminal BackupJob/RestoreJob phases and stalled
# HelmReleases rather than polling out the budget.
- script:
timeout: 50m
content: |
set -eu
cd ../../..
[ -x examples/backups/postgres/run-all.sh ] || { echo "postgres backup example scripts not found at examples/backups/postgres" >&2; exit 1; }
COZY_E2E_BACKUP_PREFLIGHT=1 NAMESPACE="$NAMESPACE" S3_ENDPOINT="https://seaweedfs-s3.tenant-root:8333" \
examples/backups/postgres/run-all.sh
check:
($error == null): true
finally:
# Resumes the driver-suspended HelmReleases before deleting the RestoreJob
# so the apps are not stranded, then removes every resource the harness
# created (chainsaw did not apply them, so they are not part of automatic
# cleanup).
- description: backup-flow cleanup
script:
timeout: 8m
content: |
set -eu
cd ../../..
NAMESPACE="$NAMESPACE" examples/backups/postgres/cleanup.sh