Files
Andrei Kvapil 3197c48bc5 test(migrations): cover migration 54 redis adopt (label+pin, CRD probe fail-closed)
Drive migration 54 end-to-end against a fake kubectl in a jq-enabled build of
the migrations image's alpine base (busybox ash, the production interpreter),
mirroring the docker+fake-kubectl migration-test harness.

Pinned properties:
  - an absent CRD stamps 55 without patching anything;
  - a failing CRD probe aborts under set -euo pipefail before stamping (a
    one-off apiserver blip must not read as "absent" and skip adoption);
  - every RedisFailover across namespaces is labelled onto the freshworks
    operator-group, while the engine is pinned to redis:6.2.6-alpine only where
    the image is empty (both roles for Harbor; sentinel only where redis.image
    is preset, which is left untouched; label-only when both are preset);
  - a failed adoption patch aborts before stamping so the Job retries.

Auto-discovered by the Makefile hack/*.bats wildcard; no registration needed.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-19 10:15:59 +02:00

140 lines
6.1 KiB
Bash
Executable File

#!/bin/sh
# Fake kubectl for hack/migration-54-redis-adopt.bats. It serves only the calls
# migration 54 (and lib/cozystack-version.sh) make; behaviour is driven by
# FAKE_* env and every invocation is appended to $FAKE_CMDLOG so the test can
# assert on which RedisFailovers were patched and with what payload, and which
# version was stamped.
#
# FAKE_CMDLOG file to append a one-line record of each call to
# FAKE_CRD_ABSENT non-empty => the CRD probe prints nothing at rc 0, i.e.
# the spotahome RedisFailover CRD is genuinely absent
# (nothing to adopt). Mutually exclusive with the next.
# FAKE_CRD_PROBE_FAIL non-empty => the CRD probe itself FAILS (rc != 0). A
# real apiserver error is NOT the same answer as "absent":
# the migration must abort under set -euo pipefail rather
# than stamp past an un-probed fleet.
# FAKE_RFS newline list of "<ns> <name> <redis_img> <sentinel_img>"
# — one per RedisFailover the -A fleet scan reports and the
# per-CR read serves. A field of "-" (or empty) models an
# UNSET image, so the migration's pin-where-empty branch is
# exercised; any other value is a preset image that must be
# left untouched.
# FAKE_LABEL_FAIL non-empty => every `patch` exits 1 with this stderr. The
# adoption label/pin is the whole point of the migration:
# a failed patch must abort before stamping so the Job
# retries rather than leaving the fleet invisible to the
# new operator.
#
# Any call the migration makes that is not modelled below is drift between the
# script and this fake and fails loudly, rather than letting the migration run
# against a call the test never reasoned about.
set -u
log() { [ -n "${FAKE_CMDLOG:-}" ] && printf '%s\n' "$*" >> "$FAKE_CMDLOG"; }
log "KUBECTL $*"
args="$*"
# arg_after KEY -- echoes the token following KEY in the argument vector.
arg_after() { k="$1"; shift; p=""; for a in "$@"; do [ "$p" = "$k" ] && { printf '%s' "$a"; return; }; p="$a"; done; }
# spec_image_obj VALUE -- a JSON object for a spec.<role> node. "-" or empty
# models an unset image (empty object, so `.spec.<role>.image // ""` is ""); any
# other value is a preset image the migration must not overwrite.
spec_image_obj() {
case "${1:-}" in
""|"-") printf '{}' ;;
*) printf '{"image":"%s"}' "$1" ;;
esac
}
case "$args" in
# CRD probe: `get crd <fqdn> --ignore-not-found -o name`. Empty + rc 0 => the
# CRD is absent (nothing to adopt); a non-zero exit models a genuine apiserver
# error, which under the migration's set -euo pipefail must abort — the exact
# failure the --ignore-not-found form exists to keep distinct from "absent".
*"get crd redisfailovers.databases.spotahome.com"*)
if [ -n "${FAKE_CRD_PROBE_FAIL:-}" ]; then
printf 'fake kubectl: the server could not find the requested resource\n' >&2
exit 1
fi
if [ -n "${FAKE_CRD_ABSENT:-}" ]; then
exit 0
fi
printf 'customresourcedefinition.apiextensions.k8s.io/redisfailovers.databases.spotahome.com\n'
exit 0
;;
# Fleet scan: every RedisFailover across all namespaces, as JSON. The
# migration pipes this through jq for "<ns> <name>", so only those two fields
# are consumed here; the per-CR read below serves the spec.
*"get redisfailovers.databases.spotahome.com"*"--all-namespaces"*)
printf '{"items":['
printf '%s\n' "${FAKE_RFS:-}" | {
sep=""
while read -r ns name ri si; do
[ -n "$ns" ] || continue
printf '%s{"metadata":{"namespace":"%s","name":"%s"}}' "$sep" "$ns" "$name"
sep=","
done
}
printf ']}\n'
exit 0
;;
# Per-CR read: `-n <ns> get <fqdn> <name> -o json`. Serves the spec so the
# migration can decide, per role, whether the image is empty (pin) or preset
# (leave). Reads the same FAKE_RFS row the fleet scan reported.
*"get redisfailovers.databases.spotahome.com"*)
ns=$(arg_after -n "$@")
name=$(arg_after redisfailovers.databases.spotahome.com "$@")
out=$(printf '%s\n' "${FAKE_RFS:-}" | {
while read -r rns rname rri rsi; do
[ "$rns" = "$ns" ] && [ "$rname" = "$name" ] || continue
printf '{"metadata":{"namespace":"%s","name":"%s"},"spec":{"redis":%s,"sentinel":%s}}\n' \
"$rns" "$rname" "$(spec_image_obj "$rri")" "$(spec_image_obj "$rsi")"
break
done
})
# A read with no matching FAKE_RFS row is a test-harness typo, not a cluster
# state: serving empty at rc 0 would flow into the migration's pin-both
# branch as a misleading green. Fail loudly, like every unmodelled call.
if [ -z "$out" ]; then
printf 'fake kubectl: no RedisFailover %s/%s in FAKE_RFS\n' "$ns" "$name" >&2
exit 1
fi
printf '%s\n' "$out"
exit 0
;;
# The adoption patch. --type=merge, carrying the operator-group label and, only
# where empty, the engine pin. Logged verbatim so the test can inspect the
# payload (label present on every CR; image pin present only where it should
# be). FAKE_LABEL_FAIL models a patch the apiserver rejects.
*"patch redisfailovers.databases.spotahome.com"*)
if [ -n "${FAKE_LABEL_FAIL:-}" ]; then
printf 'fake kubectl: admission webhook denied the patch\n' >&2
exit 1
fi
ns=$(arg_after -n "$@")
name=$(arg_after redisfailovers.databases.spotahome.com "$@")
patch=$(arg_after -p "$@")
log "PATCH $ns $name $patch"
exit 0
;;
# Version stamp: kubectl apply --filename -. Record the number actually
# stamped; a wrong number would loop run-migrations.sh forever and a bare
# "STAMP" assertion would not notice.
*"apply --filename -"*)
manifest=$(cat)
v=$(printf '%s\n' "$manifest" | sed -n 's/^ version: "\{0,1\}\([0-9]\{1,\}\)"\{0,1\}$/\1/p' | head -1)
log "STAMP ${v:-unknown}"
exit 0
;;
esac
log "UNHANDLED $args"
printf 'fake kubectl: unmodelled call: %s\n' "$args" >&2
exit 1