#!/usr/bin/env bats # ----------------------------------------------------------------------------- # Unit tests for packages/core/platform/images/migrations/run-migrations.sh. # # The migration hook Job retries a failed pod with the environment rendered for # the first one, so CURRENT_VERSION is what the cluster was on before any # migration of this upgrade ran. Every migration that finished has stamped the # cozystack-version ConfigMap since, and a retried pod must start from that # stamp, or it runs finished migrations a second time. # # The runner executes in the migrations image's alpine base (busybox ash, the # interpreter it ships with) against stand-in migrations and a fake kubectl # that keeps the stamped version in a file, so one pod's stamps are what the # next pod reads. The harness shape is hack/migration-58-http-cache-freeze.bats's. # # Run with: bats hack/run-migrations.bats from the repo root. # ----------------------------------------------------------------------------- load test_helper FAKEBIN="$PWD/hack/testdata/run-migrations" RUNNER="$PWD/packages/core/platform/images/migrations/run-migrations.sh" LIB="$PWD/packages/core/platform/images/migrations/migrations/lib" ALPINE=$(sed -n 's/^FROM \(alpine:[^ ]*\).*$/\1/p' \ "$PWD/packages/core/platform/images/migrations/Dockerfile" | head -1) WORKROOT="${TMPDIR:-/tmp}/cozy-run-migrations-$$" cozy_cleanup() { rm -rf "$WORKROOT" return 0 } # prep prep() { docker info >/dev/null 2>&1 || { echo "docker is required: these tests run run-migrations.sh inside $ALPINE," >&2 echo "the migrations image's base." >&2 return 1 } mkdir -p "$WORKROOT" WORK=$(mktemp -d "$WORKROOT/XXXXXX") mkdir -p "$WORK/migrations" cp -R "$LIB" "$WORK/migrations/lib" for n in 57 58 59; do cp "$FAKEBIN/migration" "$WORK/migrations/$n" done : > "$WORK/ran" [ "$1" = none ] || printf '%s' "$1" > "$WORK/state" chmod +x "$FAKEBIN/kubectl" unset FAKE_FAIL_AT FAKE_READ_FAIL FAKE_EXIST_FAIL || true return 0 } # run_pod -- one pod of the hook Job. run_pod() { _run_pod_rc=0 docker run --rm --network none \ --user "$(id -u):$(id -g)" \ -v "$RUNNER:/usr/bin/run-migrations.sh:ro" \ -v "$WORK/migrations:/migrations" \ -v "$FAKEBIN:/fakebin:ro" \ -v "$WORK:/work" \ -e PATH=/fakebin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ -e FAKE_STATE=/work/state \ -e FAKE_FAIL_AT="${FAKE_FAIL_AT-}" \ -e FAKE_READ_FAIL="${FAKE_READ_FAIL-}" \ -e FAKE_EXIST_FAIL="${FAKE_EXIST_FAIL-}" \ -e NAMESPACE=cozy-system \ -e CURRENT_VERSION="$1" \ -e TARGET_VERSION="$2" \ "$ALPINE" /bin/sh /usr/bin/run-migrations.sh || _run_pod_rc=$? return "$_run_pod_rc" } ran() { tr '\n' ' ' < "$WORK/ran" | sed 's/ $//' return 0 } @test "a retried pod starts from the version the failed pod stamped" { prep 57 export FAKE_FAIL_AT=59 rc=0 run_pod 57 60 || rc=$? [ "$rc" -ne 0 ] [ "$(cat "$WORK/state")" = 59 ] unset FAKE_FAIL_AT rc=0 run_pod 57 60 || rc=$? echo "ran: $(ran)" [ "$rc" -eq 0 ] [ "$(ran)" = "57 58 59 59" ] [ "$(cat "$WORK/state")" = 60 ] rm -rf "$WORK" } @test "a pod that finds the target already stamped runs nothing" { prep 60 rc=0 run_pod 57 60 || rc=$? echo "ran: $(ran)" [ "$rc" -eq 0 ] [ -z "$(ran)" ] rm -rf "$WORK" } @test "a ConfigMap with no version falls back to the rendered one" { prep "" rc=0 run_pod 58 60 || rc=$? echo "ran: $(ran)" [ "$rc" -eq 0 ] [ "$(ran)" = "58 59" ] rm -rf "$WORK" } # A non-integer start fails the -ge test inside an if and makes seq fail # inside the for list, and neither trips set -e: without the fallback such a # stamp gives a green Job that ran nothing and moved no stamp. @test "a ConfigMap with a non-integer version falls back to the rendered one" { prep v58 rc=0 run_pod 58 60 || rc=$? echo "ran: $(ran)" [ "$rc" -eq 0 ] [ "$(ran)" = "58 59" ] rm -rf "$WORK" } # The same unguarded -ge and empty seq turn an all-digit stamp past the shell's # integer range into a green Job that ran nothing. Falling back is no answer: # the chart's int renders such a stamp as 0, which would replay every migration. @test "a stamped version outside the shell's integer range stops before any migration" { prep 999999999999999999999999999999 rc=0 run_pod 0 60 || rc=$? echo "ran: $(ran)" [ "$rc" -ne 0 ] [ -z "$(ran)" ] rm -rf "$WORK" } @test "a failed read of the stamped version stops before any migration" { prep 58 export FAKE_READ_FAIL="Error from server (Timeout): the server was unable to return a response in the time allotted" rc=0 run_pod 57 60 || rc=$? echo "ran: $(ran)" [ "$rc" -ne 0 ] [ -z "$(ran)" ] [ "$(cat "$WORK/state")" = 58 ] rm -rf "$WORK" } # Only NotFound means a fresh cluster. Reading any other failure of the # existence check as absence stamps the target over a cluster whose # migrations never ran, and the Job ends green. @test "a failed existence check stops before stamping or running anything" { prep 57 export FAKE_EXIST_FAIL="Unable to connect to the server: net/http: TLS handshake timeout" rc=0 run_pod 57 60 || rc=$? echo "ran: $(ran)" [ "$rc" -ne 0 ] [ -z "$(ran)" ] [ "$(cat "$WORK/state")" = 57 ] rm -rf "$WORK" } @test "with no ConfigMap it stamps the target and runs nothing" { prep none rc=0 run_pod 57 60 || rc=$? echo "ran: $(ran)" [ "$rc" -eq 0 ] [ -z "$(ran)" ] [ "$(cat "$WORK/state")" = 60 ] rm -rf "$WORK" }