mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-23 03:27:50 +09:00
The shell translator rewrites test bodies and loses Bats reporting. The unit lane needs complete TAP results and JUnit reports without changing how live-cluster suites run. Sourced production helpers still need a dash pass, and unit test bodies need nounset without exporting shell options into subprocesses. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
245 lines
11 KiB
Bash
245 lines
11 KiB
Bash
#!/usr/bin/env bats
|
|
# -----------------------------------------------------------------------------
|
|
# Unit tests for platform migration 54 --> 55 (adopt every existing
|
|
# RedisFailover onto the freshworks-oss redis-operator's group filter AND pin
|
|
# its engine, before the new operator starts).
|
|
#
|
|
# redis-operator v3.3.5 watches RedisFailovers through a server-side label
|
|
# selector (redis-failover.freshworks.com/operator-group=cozystack); a CR
|
|
# without that label never reaches the operator's informer. The same window is
|
|
# dangerous for the engine version: v3.3.5's compiled-in default moved
|
|
# 6.2.6 -> 7.2.4, which rewrites dump.rdb into a format 6.2.6 cannot load. So the
|
|
# migration, in one pass ahead of the new operator, ALWAYS stamps the label and
|
|
# pins spec.redis.image / spec.sentinel.image to redis:6.2.6-alpine ONLY where
|
|
# that field is empty — leaving a preset image (apps/redis pins its data image
|
|
# via versionMap) untouched.
|
|
#
|
|
# Three properties are pinned here:
|
|
#
|
|
# 1. FAIL CLOSED ON THE PROBE. `kubectl get crd ... --ignore-not-found -o name`
|
|
# separates a genuinely absent CRD (rc 0, empty -> nothing to adopt, stamp
|
|
# and exit) from a real apiserver error (rc != 0 -> abort under
|
|
# set -euo pipefail). A one-off blip read as "absent" would stamp the
|
|
# version and leave the whole fleet unlabelled and invisible to the new
|
|
# operator, permanently — the exact failure this migration exists to
|
|
# prevent.
|
|
#
|
|
# 2. LABEL ALWAYS, PIN ONLY WHERE EMPTY. Every adopted CR's patch carries the
|
|
# operator-group label. The engine pin is added only for a role whose image
|
|
# is empty: a both-empty CR (Harbor) gets both images pinned; a
|
|
# sentinel-only-empty CR (apps/redis, redis.image preset) gets ONLY the
|
|
# sentinel pinned and its preset redis.image is never overwritten; a
|
|
# fully-preset CR gets the label and no pin at all.
|
|
#
|
|
# 3. FAIL CLOSED ON THE PATCH. Migrations never re-run, so a swallowed patch
|
|
# error would stamp the version on a half-adopted fleet and never look back.
|
|
# A failed patch must abort BEFORE stamping so the Job retries.
|
|
#
|
|
# These drive the real migration script end-to-end against a fake kubectl
|
|
# (hack/testdata/migration-54-redis/), mocking only the cluster boundary.
|
|
#
|
|
# SHELL. Production runs migration 54 under /bin/sh = busybox ash: the migrations
|
|
# image is FROM alpine and run-migrations.sh execs `/migrations/<n>` BY PATH, so
|
|
# the kernel honours the `#!/bin/sh` shebang. `set -euo pipefail` and its
|
|
# errexit semantics differ from bash, and pipefail is load-bearing: the fleet
|
|
# scan pipes `kubectl ... -o json` into jq. So run_migration() runs the script by
|
|
# path inside the image's own pinned base rather than through a host shell — same
|
|
# base image, same interpreter, same invocation form.
|
|
#
|
|
# jq. Migration 54 pipes through jq (fleet scan, per-CR image read, patch
|
|
# construction). The raw alpine base ships without it while the built migrations
|
|
# image installs it via `apk add`, so prep() bakes jq onto the pinned base once
|
|
# (the only step that touches the network) and run_migration() uses that image,
|
|
# still --network none.
|
|
#
|
|
# CI runs this file under Bats through `make bats-unit-tests`. It also remains
|
|
# compatible with the legacy `hack/cozytest.sh` translator, whose awk parser
|
|
# rewrites a bare `}` into `return 0` plus `}`. The tests avoid `run`, `$status`,
|
|
# and setup() for that path. A helper whose status matters captures and returns
|
|
# it before its closing brace so the injected return cannot mask it.
|
|
#
|
|
# Run with: bats hack/migration-54-redis-adopt.bats
|
|
# -----------------------------------------------------------------------------
|
|
|
|
load test_helper
|
|
|
|
FAKEBIN="$PWD/hack/testdata/migration-54-redis"
|
|
MIG_DIR="$PWD/packages/core/platform/images/migrations/migrations"
|
|
|
|
# The production base image, read out of the migrations Dockerfile rather than
|
|
# repeated here, so the interpreter under test cannot drift from the one the
|
|
# migrations actually ship on when that pin is bumped.
|
|
ALPINE=$(sed -n 's/^FROM \(alpine:[^ ]*\).*$/\1/p' \
|
|
"$PWD/packages/core/platform/images/migrations/Dockerfile" | head -1)
|
|
|
|
# jq-enabled build of that base. The tag is derived from the pinned ref so a
|
|
# digest bump rebuilds it instead of reusing a stale layer.
|
|
TESTIMG="cozystack-migration54-test:$(printf '%s' "$ALPINE" | sed 's/[^a-zA-Z0-9]/-/g')"
|
|
|
|
# run_migration <n> -- run migrations/<n> the way run-migrations.sh does.
|
|
#
|
|
# By path, not `sh <file>`: that is what makes the shebang, and therefore the
|
|
# interpreter, part of what is under test. The fake kubectl goes on PATH inside
|
|
# the container and $WORK is bind-mounted, so $FAKE_CMDLOG is the same file the
|
|
# assertions read back on the host. --network none because nothing here may reach
|
|
# a real cluster; --user keeps $WORK removable by the test afterwards.
|
|
#
|
|
# The explicit `return` is load-bearing: cozytest.sh's awk generator rewrites
|
|
# every bare `}` in column 0 into `return 0` + `}`, so a helper that falls off
|
|
# its own end returns 0 no matter what it ran, and every fail-closed assertion
|
|
# below would pass vacuously. Capture the status and return it by hand.
|
|
run_migration() {
|
|
_run_migration_rc=0
|
|
docker run --rm --network none \
|
|
--user "$(id -u):$(id -g)" \
|
|
-v "$MIG_DIR:/migrations:ro" \
|
|
-v "$FAKEBIN:/fakebin:ro" \
|
|
-v "$WORK:/work" \
|
|
-e PATH=/fakebin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
|
-e FAKE_CMDLOG=/work/cmdlog \
|
|
-e NAMESPACE="${NAMESPACE-}" \
|
|
-e FAKE_RFS="${FAKE_RFS-}" \
|
|
-e FAKE_CRD_ABSENT="${FAKE_CRD_ABSENT-}" \
|
|
-e FAKE_CRD_PROBE_FAIL="${FAKE_CRD_PROBE_FAIL-}" \
|
|
-e FAKE_LABEL_FAIL="${FAKE_LABEL_FAIL-}" \
|
|
"$TESTIMG" "/migrations/$1" || _run_migration_rc=$?
|
|
return "$_run_migration_rc"
|
|
}
|
|
|
|
# prep resets env to a clean scenario. Tests set FAKE_* afterwards.
|
|
prep() {
|
|
# Fail here rather than at the first docker run, so the reason is legible.
|
|
docker info >/dev/null 2>&1 || {
|
|
echo "docker is required: these tests run migration 54 inside a jq-enabled" >&2
|
|
echo "build of $ALPINE (the migrations image's base), so that it exercises" >&2
|
|
echo "busybox ash — the interpreter run-migrations.sh actually gives it." >&2
|
|
return 1
|
|
}
|
|
# Bake jq onto the pinned base. Cached after the first run, so this is a no-op
|
|
# on every subsequent test; only the first build touches the network.
|
|
docker build -q -t "$TESTIMG" - >/dev/null <<DOCKERFILE
|
|
FROM $ALPINE
|
|
RUN apk add --no-cache jq
|
|
DOCKERFILE
|
|
chmod +x "$FAKEBIN/kubectl"
|
|
WORK=$(mktemp -d)
|
|
export FAKE_CMDLOG="$WORK/cmdlog"
|
|
: > "$FAKE_CMDLOG"
|
|
export NAMESPACE=cozy-system
|
|
export FAKE_RFS=""
|
|
unset FAKE_CRD_ABSENT FAKE_CRD_PROBE_FAIL FAKE_LABEL_FAIL || true
|
|
return 0
|
|
}
|
|
|
|
# patch_for <ns> <name> -- the merge-patch payload the migration sent for that
|
|
# CR (the JSON tail of its `PATCH <ns> <name> <json>` cmdlog line). Used only for
|
|
# its stdout, so the awk-injected `return 0` is harmless.
|
|
patch_for() {
|
|
grep -E "^PATCH $1 $2 " "$FAKE_CMDLOG" | head -1 | cut -d' ' -f4-
|
|
}
|
|
|
|
# --- 1. fail closed on the probe --------------------------------------------
|
|
|
|
@test "absent CRD stamps 55 without patching anything" {
|
|
prep
|
|
export FAKE_CRD_ABSENT=1
|
|
rc=0
|
|
run_migration 54 >"$WORK/out" 2>&1 || rc=$?
|
|
cat "$WORK/out"; cat "$FAKE_CMDLOG"
|
|
[ "$rc" -eq 0 ]
|
|
# Nothing to adopt: no patch was ever issued...
|
|
[ "$(grep -cE -- '^PATCH ' "$FAKE_CMDLOG")" -eq 0 ]
|
|
# ...but the version is stamped to 55 so the runner advances. Asserting the
|
|
# number, not a bare "STAMP": a wrong version would loop run-migrations.sh.
|
|
grep -qF -- "STAMP 55" "$FAKE_CMDLOG"
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
# A real apiserver error on the CRD probe is NOT "absent". The migration must
|
|
# abort under set -euo pipefail before stamping, so the Job retries rather than
|
|
# stamping past an un-probed, un-adopted fleet.
|
|
@test "a failing CRD probe aborts before stamping" {
|
|
prep
|
|
export FAKE_CRD_PROBE_FAIL=1
|
|
rc=0
|
|
run_migration 54 >"$WORK/out" 2>&1 || rc=$?
|
|
cat "$WORK/out"; cat "$FAKE_CMDLOG"
|
|
# Must propagate: a swallowed probe error would stamp past the whole fleet.
|
|
[ "$rc" -ne 0 ]
|
|
# Nothing was patched and nothing was stamped.
|
|
[ "$(grep -cE -- '^PATCH ' "$FAKE_CMDLOG")" -eq 0 ]
|
|
[ "$(grep -cF -- 'STAMP 55' "$FAKE_CMDLOG")" -eq 0 ]
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
# --- 2. label always, pin only where empty ----------------------------------
|
|
|
|
@test "adopts every RedisFailover across namespaces: label always, engine pin only where empty" {
|
|
prep
|
|
# Three CRs in three namespaces exercising each image shape:
|
|
# harbor both images empty (Harbor) -> pin BOTH
|
|
# my-redis redis preset, sentinel empty -> pin ONLY sentinel
|
|
# full-redis both images preset -> label only, no pin
|
|
export FAKE_RFS="tenant-harbor harbor - -
|
|
tenant-redis my-redis redis:8.4.0 -
|
|
tenant-full full-redis redis:7.0.0 redis:7.2.0"
|
|
rc=0
|
|
run_migration 54 >"$WORK/out" 2>&1 || rc=$?
|
|
cat "$WORK/out"; cat "$FAKE_CMDLOG"
|
|
[ "$rc" -eq 0 ]
|
|
|
|
# Exactly the three CRs were patched, no more.
|
|
[ "$(grep -cE '^PATCH ' "$FAKE_CMDLOG")" -eq 3 ]
|
|
# Every patch carries the operator-group label (adoption is unconditional).
|
|
# Scope the count to PATCH lines: the fake also logs a raw KUBECTL line per
|
|
# call, so the label string appears twice per patch across the whole cmdlog.
|
|
[ "$(grep -E '^PATCH ' "$FAKE_CMDLOG" | grep -cF -- '"redis-failover.freshworks.com/operator-group":"cozystack"')" -eq 3 ]
|
|
|
|
# Harbor: both images empty -> both pinned to redis:6.2.6-alpine, plus label.
|
|
ph=$(patch_for tenant-harbor harbor)
|
|
echo "$ph" | grep -qF -- '"redis-failover.freshworks.com/operator-group":"cozystack"'
|
|
echo "$ph" | grep -qF -- '"redis":{"image":"redis:6.2.6-alpine"}'
|
|
echo "$ph" | grep -qF -- '"sentinel":{"image":"redis:6.2.6-alpine"}'
|
|
|
|
# apps/redis: redis.image preset, sentinel empty -> ONLY sentinel pinned. The
|
|
# preset redis.image is neither overwritten nor even mentioned (a merge patch
|
|
# only carries what changes), so there is no redis node in the payload at all.
|
|
pr=$(patch_for tenant-redis my-redis)
|
|
echo "$pr" | grep -qF -- '"redis-failover.freshworks.com/operator-group":"cozystack"'
|
|
echo "$pr" | grep -qF -- '"sentinel":{"image":"redis:6.2.6-alpine"}'
|
|
[ "$(printf '%s' "$pr" | grep -cF -- '"redis":')" -eq 0 ]
|
|
[ "$(printf '%s' "$pr" | grep -cF -- 'redis:8.4.0')" -eq 0 ]
|
|
|
|
# Fully-preset: label only. Neither image is empty, so no engine pin and no
|
|
# spec node at all.
|
|
pf=$(patch_for tenant-full full-redis)
|
|
echo "$pf" | grep -qF -- '"redis-failover.freshworks.com/operator-group":"cozystack"'
|
|
[ "$(printf '%s' "$pf" | grep -cF -- 'redis:6.2.6-alpine')" -eq 0 ]
|
|
[ "$(printf '%s' "$pf" | grep -cF -- '"spec":')" -eq 0 ]
|
|
|
|
# Version stamped to 55 once, after adoption.
|
|
grep -qF -- "STAMP 55" "$FAKE_CMDLOG"
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
# --- 3. fail closed on the patch --------------------------------------------
|
|
|
|
# The adoption patch fails. Applying the label/pin is the whole point of the
|
|
# migration, so a failed patch must abort BEFORE stamping — recoverable on the
|
|
# Job's next attempt, rather than a fleet stamped past adoption and left
|
|
# invisible to the new operator forever.
|
|
@test "a failed adoption patch aborts before stamping" {
|
|
prep
|
|
export FAKE_RFS="tenant-harbor harbor - -
|
|
tenant-redis my-redis redis:8.4.0 -"
|
|
export FAKE_LABEL_FAIL=1
|
|
rc=0
|
|
run_migration 54 >"$WORK/out" 2>&1 || rc=$?
|
|
cat "$WORK/out"; cat "$FAKE_CMDLOG"
|
|
# Must propagate: a swallowed patch error would stamp a half-adopted fleet.
|
|
[ "$rc" -ne 0 ]
|
|
# And it must not have stamped the version.
|
|
[ "$(grep -cF -- 'STAMP 55' "$FAKE_CMDLOG")" -eq 0 ]
|
|
rm -rf "$WORK"
|
|
}
|