mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 14:02:56 +09:00
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>
345 lines
15 KiB
YAML
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
|