Files
cozystack/hack/e2e-chainsaw/mongodb/chainsaw-test.yaml
T
Aleksei Sviridkin 04d469fc85 fix(tests): let the backup walkthrough waits outlive their installs
The backup round-trips drive examples/backups, whose bucket waits gave
up at 300s and whose postgres waits at 360s, under the 600s install
timeout cozystack-api gives each of those releases. The kafka example's
bucket wait passed no budget and took the helper's 300s default. A
failed install there reported only that the wait ran out.

Raise each to 660s, and grow the Chainsaw op around every walkthrough
by what its waits gained, so a failure at any later wait keeps the
headroom it had and is still reported by the script rather than cut
off by the op. The kafka and kafka-metadata Kafka waits stay: their
7h install timeout cannot be cleared inside the E2E job cap.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-01 17:40:22 +03:00

463 lines
20 KiB
YAML

# Chainsaw port of hack/e2e-apps/mongodb.bats ("Create DB MongoDB").
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
name: mongodb
spec:
catch:
- events: {}
- describe:
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
name: mongodb-test
# The controller writes its verdict into the sentinel's status conditions
# (SourceNotFound, SourceRejected, ProjectionCollision, ...). Without this a
# failed projection surfaces only as an absent Secret, with no reason.
- describe:
apiVersion: internal.cozystack.io/v1alpha1
kind: TenantProjection
- podLogs:
selector: app.kubernetes.io/instance=mongodb-test
tail: 50
steps:
- name: create-mongodb
try:
- apply:
file: mongodb.yaml
- name: wait-helmrelease-ready
try:
# 11m: above the 600s install timeout cozystack-api gives this release;
# see "Sizing an HR-Ready budget" in docs/agents/e2e-testing.md.
- assert:
timeout: 11m
resource:
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: mongodb-test
status:
(conditions[?type == 'Ready']):
- status: "True"
- name: verify-service-and-statefulset
try:
- assert:
timeout: 2m
resource:
apiVersion: v1
kind: Service
metadata:
name: mongodb-test-rs0
spec:
(ports[0].port): 27017
- assert:
timeout: 3m
resource:
apiVersion: v1
kind: Endpoints
metadata:
name: mongodb-test-rs0
(subsets[0].addresses[0].ip != null): true
- assert:
timeout: 5m
resource:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongodb-test-rs0
status:
readyReplicas: 1
# The TenantProjection declares the trust-anchor source by name
# (<release>-ssl, key ca.crt). Nothing else in the repo checks that this is
# what the operator actually produces: the sentinel and any unit fixture
# would encode the same assumption and so cannot catch each other being
# wrong. This step is the independent witness — a Percona change to either
# the Secret name or the key breaks it here rather than silently producing an
# empty trust anchor.
- name: verify-trust-anchor-source-shape
try:
- assert:
timeout: 5m
resource:
apiVersion: v1
kind: Secret
metadata:
name: mongodb-test-ssl
(data."ca.crt" != null): true
# The README tells operators no manual cleanup of the TLS Secrets is needed,
# which reverses the instruction it replaced. That promise rests entirely on
# an ownerReference: the operator's deletion finalizer enumerates only the
# users, internal-<release>, internal-<release>-users and encryption-key
# Secrets, so nothing else would reclaim these three.
#
# The reference is patched on at reconcile time rather than set at creation —
# cert-manager creates the Secret unowned and WaitForCerts adds the CR
# reference afterwards — so this asserts the outcome on the path a normal
# install takes, and goes red if Percona stops patching. Matched on the owner
# kind rather than on a non-empty list, because the Secret acquiring some
# other owner is the failure this is meant to catch, not a pass.
#
# Pinned to PerconaServerMongoDB specifically because the platform ships
# cert-manager with enableCertificateOwnerRef: false, which is the leg where
# the CR owns the Secret directly. Were that flag ever flipped, the owner
# becomes the Certificate and this step correctly goes red rather than
# quietly accepting a different ownership graph than the README describes.
- name: verify-tls-secrets-are-owned-by-the-cr
try:
- assert:
timeout: 5m
resource:
apiVersion: v1
kind: Secret
metadata:
name: mongodb-test-ca-cert
(length(metadata.ownerReferences[?kind == 'PerconaServerMongoDB'] || `[]`)): 1
- assert:
timeout: 5m
resource:
apiVersion: v1
kind: Secret
metadata:
name: mongodb-test-ssl
(length(metadata.ownerReferences[?kind == 'PerconaServerMongoDB'] || `[]`)): 1
- assert:
timeout: 5m
resource:
apiVersion: v1
kind: Secret
metadata:
name: mongodb-test-ssl-internal
(length(metadata.ownerReferences[?kind == 'PerconaServerMongoDB'] || `[]`)): 1
# The Secrets the operator creates for TLS all carry private key material and
# must never be reachable by a tenant. The chart tests pin this at render
# time; this pins the object that is actually deployed.
#
# Asserted as an exact list rather than as the absence of forbidden names,
# because a rule granting `secrets` with no resourceNames at all grants every
# Secret in the namespace while containing no forbidden name to match on.
#
# Two counts are asserted alongside it and neither is redundant. An
# ADDITIONAL rule naming secrets contributes no entries to the flattened name
# list, so the equality alone would still hold while every Secret became
# readable — the secrets-rule count catches that. An ADDED rule with resources
# ["*"] grants every Secret without containing the string "secrets" at all, so
# it is invisible to both — the total rule count catches that one. An existing
# rule MUTATED to ["*"] drops the secrets-rule count to zero and empties the
# flattened name list, so both assertions below fail on it as well.
- name: verify-tenant-role-grants-no-key-material
try:
- assert:
timeout: 2m
resource:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: mongodb-test-dashboard-resources
(length(rules)): 3
(length(rules[?contains(resources, 'secrets')])): 1
(rules[?contains(resources, 'secrets')].resourceNames[]):
- mongodb-test-credentials
# The Role is read-only by intent; verbs is the fourth RBAC axis and
# widening it is the same class as widening the other three.
(length(rules[?verbs != ['get', 'list', 'watch']])): 0
# The Role above is only one of the two paths a Secret can reach a tenant by.
# The other is core.cozystack.io/tenantsecrets, which surfaces whatever the
# lineage webhook stamped internal.cozystack.io/tenantresource=true from this
# application's ApplicationDefinition selectors. That is the path the
# secrets.include selector drives, and it is the one that would leak the CA
# private key if the selector ever named the operator's Secrets instead of
# the projection. Assert the key-bearing Secrets are not promoted onto it.
#
# This negative holds today and does not depend on the CA-extraction
# controller: it asserts what must NOT be labelled, not that the projection
# exists. Written as != 'true' rather than == 'false' because the stamp is
# applied when the operator adds its ownerReference, so before that pass the
# label is absent rather than false — both states are correct here.
- name: verify-key-bearing-secrets-are-not-tenant-secrets
try:
# Positive control, and it must come first. The three negatives below are
# satisfied by an ABSENT label — which is also exactly what you observe if
# the lineage webhook never stamped anything in this run at all, because a
# field was renamed or the kind was not reconciled. Without a witness that
# the mechanism is live, all three would pass while asserting nothing about
# the CA private key.
#
# The credentials Secret is the right witness: the chart renders it, it
# traces to the MongoDB CR, and the ApplicationDefinition grants it by name
# — so it MUST be stamped. This also catches the regression the exclude
# list makes newly possible, since exclude is evaluated before include and
# would silently revoke the tenant's own connection string.
- assert:
timeout: 2m
resource:
apiVersion: v1
kind: Secret
metadata:
name: mongodb-test-credentials
labels:
internal.cozystack.io/tenantresource: "true"
- assert:
timeout: 2m
resource:
apiVersion: v1
kind: Secret
metadata:
name: mongodb-test-ca-cert
(metadata.labels."internal.cozystack.io/tenantresource" != 'true'): true
- assert:
timeout: 2m
resource:
apiVersion: v1
kind: Secret
metadata:
name: mongodb-test-ssl
(metadata.labels."internal.cozystack.io/tenantresource" != 'true'): true
# All three are enumerated deliberately. The promotion is driven by a label
# selector rather than by name, so any one of them acquiring the label
# leaks a key — witnessing two of the three would leave the third unguarded.
- assert:
timeout: 2m
resource:
apiVersion: v1
kind: Secret
metadata:
name: mongodb-test-ssl-internal
(metadata.labels."internal.cozystack.io/tenantresource" != 'true'): true
# The negatives above are all satisfied by an absent label, so they say
# nothing about the anchor a tenant is supposed to get. This is the positive:
# the sentinel converged, the controller lifted the right certificate, and
# the registry serves it.
#
# tenant-ca is stamped by the controller; tenantresource is the LINEAGE
# WEBHOOK's verdict, computed from the ApplicationDefinition's secrets.include
# selector. It is "true" only if the whole chain held, and nothing short of a
# live cluster proves that link.
- name: verify-tenant-ca-projection
try:
- assert:
timeout: 2m
resource:
apiVersion: v1
kind: Secret
metadata:
name: mongodb-test.tenant-ca
labels:
internal.cozystack.io/tenant-ca: "true"
internal.cozystack.io/tenantresource: "true"
type: Opaque
# Deterministic once the assert above passes, so no retry.
- script:
content: |
set -eu
# Exactly one key, and it is ca.crt. The source carries a private key,
# so any second key is a leak in the making.
keys=$(kubectl -n "$NAMESPACE" get secret mongodb-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. This is what pins the declared
# source to reality: if Percona renames the Secret or the key, the
# controller finds nothing and this fails.
#
# Compared by fingerprint, not raw PEM — the controller re-encodes
# ca.crt from the DER it parsed, so a header or column-width change at
# the source would redden a byte comparison of a correct projection.
src=$(kubectl -n "$NAMESPACE" get secret mongodb-test-ssl -o jsonpath='{.data.ca\.crt}' | base64 -d)
if [ -z "$src" ]; then
echo "mongodb-test-ssl has no ca.crt — the declared source name or key is wrong" >&2
exit 1
fi
proj=$(kubectl -n "$NAMESPACE" get secret mongodb-test.tenant-ca -o jsonpath='{.data.ca\.crt}' | base64 -d)
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 the operator's CA certificate" >&2
exit 1
fi
# The point of the whole exercise: the source carries a private key,
# the projection must not. Asserting the source really does hold
# tls.key keeps this honest — if the operator ever stopped storing it
# there, the check below would pass while proving nothing.
if ! kubectl -n "$NAMESPACE" get secret mongodb-test-ssl \
-o go-template='{{if .data}}{{range $k, $v := .data}}{{$k}}{{"\n"}}{{end}}{{end}}' | grep -qx 'tls.key'; then
echo "mongodb-test-ssl no longer carries tls.key — this check has stopped proving anything" >&2
exit 1
fi
if kubectl -n "$NAMESPACE" get secret mongodb-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 tells a tenant to use, run as the
# command the README prints. Everything above reads the raw Secret,
# which only an admin can do.
anchor=$(kubectl -n "$NAMESPACE" get tenantsecret mongodb-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 the key-bearing Secrets. If either starts resolving,
# the label path has stopped gating on the platform's verdict.
for s in mongodb-test-ssl mongodb-test-ssl-internal mongodb-test-ca-cert; do
if kubectl -n "$NAMESPACE" get tenantsecret "$s" >/dev/null 2>&1; then
echo "key-bearing $s is being served as a tenant secret" >&2
exit 1
fi
done
# The -old snapshots are operator scratch state that the operator removes
# itself once the merged CA has landed, not residue needing manual cleanup.
# A settled install must therefore leave none behind. This is a weak witness
# on first install, where they are never created at all, but it pins the
# corrected claim and goes red if the operator stops cleaning up.
- name: verify-no-rotation-snapshots-remain
try:
- error:
# Short: a Secret that does not exist will not start existing while
# chainsaw waits, and this step is a weak witness on a first install.
timeout: 30s
resource:
apiVersion: v1
kind: Secret
metadata:
name: mongodb-test-ssl-old
- error:
timeout: 30s
resource:
apiVersion: v1
kind: Secret
metadata:
name: mongodb-test-ssl-internal-old
---
# Logical-dump backup half of the MongoDB backup flow, driving the validated
# example scripts (examples/backups/mongodb) as the harness so the test and the
# documented flow can never drift. run-all.sh runs Bucket -> source MongoDB
# (backup.enabled) + sentinel document -> ad-hoc BackupJob against the
# cozy-default BackupClass (waits Succeeded), and SKIP_RESTORE=1 stops it there
# — the in-cluster witness that a psmdb-operator backup is actually taken.
#
# The to-copy restore half is deliberately uncovered. A to-copy restore replays
# the source cluster's system users into the target, so the target's own
# generated credentials stop authenticating and the harness cannot read the
# sentinel back out of the restored copy (#3832). Returning full round-trip
# coverage once that is fixed means dropping both SKIP_RESTORE below and the
# marker check beside it: the complete flow never prints that line, so removing
# only the variable trades this failure for a marker mismatch. The test keeps
# its round-trip name for that return.
#
# Same harness-driving shape as mariadb-2-backup-roundtrip
# (hack/e2e-chainsaw/mariadb/chainsaw-test.yaml): a single run step pre-cleans
# any leftover state, drives run-all.sh, and drains in an always-reached finally.
#
# Runs in tenant-root, not the default tenant-test: the S3 client here is the
# percona-backup-mongodb agent INSIDE the mongod 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 tolerates
# its self-signed cert via backup.insecureSkipTLSVerify=true on the app (the
# psmdb s3 storage has no CA-bundle field, unlike the mariadb driver's tls
# block).
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
name: mongodb-2-backup-roundtrip
spec:
namespace: tenant-root
skipDelete: true
catch:
- events: {}
- describe:
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
name: mongodb-mongodb-src
- describe:
apiVersion: psmdb.percona.com/v1
kind: PerconaServerMongoDB
name: mongodb-mongodb-src
- describe:
apiVersion: psmdb.percona.com/v1
kind: PerconaServerMongoDBBackup
- describe:
apiVersion: psmdb.percona.com/v1
kind: PerconaServerMongoDBRestore
- describe:
apiVersion: backups.cozystack.io/v1alpha1
kind: BackupJob
- describe:
apiVersion: backups.cozystack.io/v1alpha1
kind: RestoreJob
# The dump/restore runs in the pbm agent inside the mongod pods; a failed
# upload or TLS handshake leaves its error in those Pods' logs.
- podLogs:
selector: app.kubernetes.io/instance=mongodb-mongodb-src
tail: 200
steps:
- name: run
try:
# Pre-clean: tenant-root is a shared, persistent namespace with fixed
# resource names, so a run killed before the finally below leaves stale
# state behind. A leftover BackupJob still carrying status.phase=Succeeded
# from a prior run would satisfy `wait_for_field ... Succeeded` instantly
# and pass without a real backup ever running. cleanup.sh is idempotent
# (--ignore-not-found), so this is a no-op on a clean namespace.
- script:
timeout: 8m
content: |
set -eu
cd ../../..
NAMESPACE="$NAMESPACE" examples/backups/mongodb/cleanup.sh
check:
($error == null): true
# The 42m budget covers what this step drives: bucket provisioning, a
# single-replica source MongoDB coming up (psmdb reconcile, PVC bind, image
# pull, pbm-agent sidecars) and the logical dump to S3 (convention 6:
# justified in-line). It is not lowered for the skipped restore because
# run-all.sh's own per-wait budgets up to the backup already exceed it, so
# this is the ceiling that fires either way. run-all.sh fails fast on a
# terminal BackupJob phase and stalled HelmReleases rather than polling out
# the budget. Sized with the 660s bucket and source HR waits in run-all.sh.
- script:
timeout: 42m
content: |
set -eu
cd ../../..
[ -x examples/backups/mongodb/run-all.sh ] || { echo "mongodb backup example scripts not found at examples/backups/mongodb" >&2; exit 1; }
COZY_E2E_BACKUP_PREFLIGHT=1 NAMESPACE="$NAMESPACE" S3_ENDPOINT="https://seaweedfs-s3.tenant-root:8333" \
SKIP_RESTORE=1 examples/backups/mongodb/run-all.sh
check:
($error == null): true
# Exit zero alone is also what a completed round-trip returns, so the
# marker is what tells the skipped flow from the full one — and what
# fails the step if SKIP_RESTORE stops reaching the harness. run-all.sh
# logs through stderr. The phrase is the skip branch's own line, minus
# its colon, which YAML would read as a mapping separator in the key.
(contains($stderr, 'stopping after a successful backup')): true
finally:
# Removes every resource the harness created (chainsaw did not apply them,
# so they are not part of automatic cleanup) — including the operator-side
# psmdb.percona.com Backup/Restore CRs the Cozystack jobs own only by label.
- description: backup-flow cleanup
script:
timeout: 8m
content: |
set -eu
cd ../../..
NAMESPACE="$NAMESPACE" examples/backups/mongodb/cleanup.sh