test: rehearse staging database upgrades
This commit is contained in:
parent
af9018fc43
commit
143e159b01
12
README.md
12
README.md
|
|
@ -92,6 +92,18 @@ backup_path=$(printf '%s\n' "$backup_output" | sed -n 's/^Backup: //p')
|
||||||
./scripts/restore-drill-compose.sh "$backup_path"
|
./scripts/restore-drill-compose.sh "$backup_path"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Rehearse the complete current release against that exact backup without
|
||||||
|
changing the source Compose project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/upgrade-rehearsal-compose.sh "$backup_path"
|
||||||
|
```
|
||||||
|
|
||||||
|
The rehearsal uses a uniquely named Compose project and database, applies the
|
||||||
|
current migrations, starts 2 web and 2 worker replicas, checks the configured
|
||||||
|
public-origin proxy behavior and cross-node PubSub, compares application-table
|
||||||
|
counts, and removes its containers, networks, volume, and one-run image.
|
||||||
|
|
||||||
For the reproducible encrypted S3-compatible drill, first start the isolated
|
For the reproducible encrypted S3-compatible drill, first start the isolated
|
||||||
load project and then run:
|
load project and then run:
|
||||||
|
|
||||||
|
|
|
||||||
12
compose.upgrade-rehearsal.yaml
Normal file
12
compose.upgrade-rehearsal.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
services:
|
||||||
|
migrate:
|
||||||
|
image: ${REHEARSAL_IMAGE:?Set a unique REHEARSAL_IMAGE}
|
||||||
|
pull_policy: never
|
||||||
|
|
||||||
|
web:
|
||||||
|
image: ${REHEARSAL_IMAGE:?Set a unique REHEARSAL_IMAGE}
|
||||||
|
pull_policy: never
|
||||||
|
|
||||||
|
worker:
|
||||||
|
image: ${REHEARSAL_IMAGE:?Set a unique REHEARSAL_IMAGE}
|
||||||
|
pull_policy: never
|
||||||
|
|
@ -119,3 +119,11 @@ The goal remains open while any row lacks reproducible local evidence.
|
||||||
paths, and the external protocol boundary. Scoped fixtures and one-run
|
paths, and the external protocol boundary. Scoped fixtures and one-run
|
||||||
projects were removed, retained evidence is ignored and non-secret, and Git
|
projects were removed, retained evidence is ignored and non-secret, and Git
|
||||||
was clean at the tested application commit before this documentation update.
|
was clean at the tested application commit before this documentation update.
|
||||||
|
- The public-facing database backup also passes a full isolated upgrade
|
||||||
|
rehearsal against application commit `af9018f`: the restored copy advanced
|
||||||
|
from 8 to all 10 migrations, exposed all 11 valid cursor indexes, ran two web
|
||||||
|
and two worker replicas in a four-node cluster, passed cross-node PubSub,
|
||||||
|
production HTTPS redirects, trusted-proxy pages, and both health endpoints,
|
||||||
|
and retained an empty application-table count diff. The scoped project,
|
||||||
|
volume, networks, and one-run image were removed; the ordinary Compose
|
||||||
|
project stayed on its original image and migration level.
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,40 @@ clean operation, or the drill migration runner. The drill intentionally does
|
||||||
not compare an older backup's row counts to the live source, because concurrent
|
not compare an older backup's row counts to the live source, because concurrent
|
||||||
legitimate writes or a historical archive would make that comparison invalid.
|
legitimate writes or a historical archive would make that comparison invalid.
|
||||||
|
|
||||||
|
## Isolated Compose upgrade rehearsal
|
||||||
|
|
||||||
|
After creating a current custom-format backup, run the complete current release
|
||||||
|
against an isolated restored copy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/upgrade-rehearsal-compose.sh \
|
||||||
|
output/backups/compose-YYYYMMDD-HHMMSS.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
The rehearsal validates the checksum and archive catalog, reads the public
|
||||||
|
origin configuration from ignored `.env`, and uses only the independently
|
||||||
|
generated credentials in ignored mode-`0600` `.env.e2e`. It builds a uniquely
|
||||||
|
tagged production release, creates a uniquely named Compose project and
|
||||||
|
database from `template0`, restores the archive, records application-table
|
||||||
|
counts, and then:
|
||||||
|
|
||||||
|
1. applies every current timestamped Ecto migration;
|
||||||
|
2. requires the localized category column and all 11 valid cursor indexes;
|
||||||
|
3. starts 2 web and 2 worker replicas behind the isolated Traefik instance;
|
||||||
|
4. requires the four-node BEAM cluster and cross-node PubSub probe;
|
||||||
|
5. checks the production HTTP-to-HTTPS redirect, trusted-proxy public pages,
|
||||||
|
and both health endpoints;
|
||||||
|
6. requires an empty before/after diff for every public application table
|
||||||
|
except the expected migration and Oban-internal tables;
|
||||||
|
7. removes and verifies removal of the exact containers, networks, database
|
||||||
|
volume, and one-run image.
|
||||||
|
|
||||||
|
The input archive is read-only and is not copied into the evidence directory.
|
||||||
|
The ordinary Compose project, source database, public route, and running
|
||||||
|
containers are outside the generated project scope. Non-secret evidence is
|
||||||
|
retained under ignored mode-`0700`
|
||||||
|
`output/upgrade-rehearsal/<run-id>/`, with files mode `0600`.
|
||||||
|
|
||||||
## Service checks
|
## Service checks
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ results from product limits and unknown production properties.
|
||||||
- `mix compile --force --warnings-as-errors` and
|
- `mix compile --force --warnings-as-errors` and
|
||||||
`mix format --check-formatted`: passed against the same final source.
|
`mix format --check-formatted`: passed against the same final source.
|
||||||
- `./scripts/quality.sh` passed ShellCheck 0.11.0, Hadolint 2.14.0 at warning
|
- `./scripts/quality.sh` passed ShellCheck 0.11.0, Hadolint 2.14.0 at warning
|
||||||
threshold, actionlint 1.7.12, all seven Compose renders, Helm lint, Trivy
|
threshold, actionlint 1.7.12, all eight Compose renders, Helm lint, Trivy
|
||||||
source/rendered-manifest scanning, xref, Credo high-priority checks, Sobelow
|
source/rendered-manifest scanning, xref, Credo high-priority checks, Sobelow
|
||||||
strict/private checks, Hex audit, 167 Phoenix tests, both npm audits, and the
|
strict/private checks, Hex audit, 167 Phoenix tests, both npm audits, and the
|
||||||
backup/MinIO/mc/external-mock/release-image vulnerability scans. The rendered
|
backup/MinIO/mc/external-mock/release-image vulnerability scans. The rendered
|
||||||
|
|
@ -292,6 +292,19 @@ readiness, and removed that database. A final read-only source check still
|
||||||
reported 8 migrations and the same user/request/message counts; the four
|
reported 8 migrations and the same user/request/message counts; the four
|
||||||
running application container IDs were unchanged.
|
running application container IDs were unchanged.
|
||||||
|
|
||||||
|
The same archive then passed
|
||||||
|
`./scripts/upgrade-rehearsal-compose.sh` against application commit `af9018f`.
|
||||||
|
The isolated restored copy advanced from 8 to all 10 current migrations and
|
||||||
|
reported all 11 cursor indexes valid. Two web and two worker replicas formed a
|
||||||
|
four-node BEAM cluster, the cross-node PubSub probe passed, public requests
|
||||||
|
produced the expected production HTTPS redirects, trusted-proxy pages and both
|
||||||
|
health endpoints returned HTTP 200, and the application-table count diff was
|
||||||
|
empty. The exact project, database volume, networks, and one-run image were
|
||||||
|
absent after cleanup. Evidence is retained at
|
||||||
|
`output/upgrade-rehearsal/20260719062507-3274364`. The ordinary Compose
|
||||||
|
container IDs, 8-migration state, and user/request/message counts remained
|
||||||
|
unchanged.
|
||||||
|
|
||||||
After the Activity rollout, headed Chrome rendered the updated public
|
After the Activity rollout, headed Chrome rendered the updated public
|
||||||
navigation, followed the Activity link to the authenticated route, and received
|
navigation, followed the Activity link to the authenticated route, and received
|
||||||
the expected login redirect and flash with zero console errors or warnings.
|
the expected login redirect and flash with zero console errors or warnings.
|
||||||
|
|
@ -441,8 +454,10 @@ The complete local regression was repeated against application commit
|
||||||
- The ordinary public-facing Compose project was checked read-only: local
|
- The ordinary public-facing Compose project was checked read-only: local
|
||||||
live/readiness, four-node PubSub, public HTTPS home/Safety/readiness, DNS, and
|
live/readiness, four-node PubSub, public HTTPS home/Safety/readiness, DNS, and
|
||||||
TLS passed. Its current database was backed up and restore-tested through all
|
TLS passed. Its current database was backed up and restore-tested through all
|
||||||
10 source migrations in a temporary database; its own existing rows,
|
10 source migrations, then ran the current 2-web/2-worker release on an
|
||||||
migration level, and running application containers remained unchanged.
|
isolated restored copy with an empty application-table diff and passing
|
||||||
|
HTTP/cluster/PubSub checks. Its own existing rows, migration level, and
|
||||||
|
running application containers remained unchanged.
|
||||||
|
|
||||||
## Known work before a public production launch
|
## Known work before a public production launch
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,9 @@ docker compose --env-file .env.example -f compose.yaml config --quiet
|
||||||
mkdir -p "$scan_dir/e2e-output"
|
mkdir -p "$scan_dir/e2e-output"
|
||||||
E2E_OUTPUT_DIR="$scan_dir/e2e-output" docker compose --env-file .env.e2e.example \
|
E2E_OUTPUT_DIR="$scan_dir/e2e-output" docker compose --env-file .env.e2e.example \
|
||||||
-f compose.yaml -f compose.e2e.yaml config --quiet
|
-f compose.yaml -f compose.e2e.yaml config --quiet
|
||||||
|
REHEARSAL_IMAGE=who-need-help:rehearsal-render \
|
||||||
|
docker compose --env-file .env.e2e.example \
|
||||||
|
-f compose.yaml -f compose.upgrade-rehearsal.yaml config --quiet
|
||||||
docker compose --env-file .env.load.example \
|
docker compose --env-file .env.load.example \
|
||||||
-f compose.yaml -f compose.load.yaml config --quiet
|
-f compose.yaml -f compose.load.yaml config --quiet
|
||||||
mkdir -p "$scan_dir/observability-runtime/prometheus" \
|
mkdir -p "$scan_dir/observability-runtime/prometheus" \
|
||||||
|
|
|
||||||
439
scripts/upgrade-rehearsal-compose.sh
Executable file
439
scripts/upgrade-rehearsal-compose.sh
Executable file
|
|
@ -0,0 +1,439 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
echo "Usage: $0 PATH_TO_CUSTOM_FORMAT_DUMP" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
dump=$1
|
||||||
|
checksum="$dump.sha256"
|
||||||
|
|
||||||
|
if [ ! -f "$dump" ] || [ ! -s "$dump" ]; then
|
||||||
|
echo "Dump does not exist or is empty: $dump" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$checksum" ]; then
|
||||||
|
echo "Checksum manifest is required: $checksum" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$ROOT/scripts/ensure-local-e2e-env.sh" >/dev/null
|
||||||
|
|
||||||
|
PUBLIC_ENV_FILE="$ROOT/.env"
|
||||||
|
ENV_FILE="$ROOT/.env.e2e"
|
||||||
|
|
||||||
|
if [ ! -f "$PUBLIC_ENV_FILE" ]; then
|
||||||
|
echo "Missing $PUBLIC_ENV_FILE." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rehearsal_phx_host=$(
|
||||||
|
sh -c '. "$1"; printf %s "${PHX_HOST:?PHX_HOST is missing from .env}"' \
|
||||||
|
sh "$PUBLIC_ENV_FILE"
|
||||||
|
)
|
||||||
|
rehearsal_phx_scheme=$(
|
||||||
|
sh -c '. "$1"; printf %s "${PHX_SCHEME:?PHX_SCHEME is missing from .env}"' \
|
||||||
|
sh "$PUBLIC_ENV_FILE"
|
||||||
|
)
|
||||||
|
rehearsal_phx_url_port=$(
|
||||||
|
sh -c '. "$1"; printf %s "${PHX_URL_PORT:?PHX_URL_PORT is missing from .env}"' \
|
||||||
|
sh "$PUBLIC_ENV_FILE"
|
||||||
|
)
|
||||||
|
rehearsal_map_tile_url=$(
|
||||||
|
sh -c \
|
||||||
|
'. "$1"; printf %s "${MAP_TILE_URL:-https://tile.openstreetmap.org/{z}/{x}/{y}.png}"' \
|
||||||
|
sh "$PUBLIC_ENV_FILE"
|
||||||
|
)
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
: "${POSTGRES_USER:?POSTGRES_USER is missing from .env.e2e}"
|
||||||
|
: "${DATABASE_URL:?DATABASE_URL is missing from .env.e2e}"
|
||||||
|
|
||||||
|
dump_dir=$(CDPATH='' cd -- "$(dirname -- "$dump")" && pwd)
|
||||||
|
dump_name=$(basename -- "$dump")
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$dump_dir"
|
||||||
|
sha256sum --check --status "$(basename -- "$checksum")"
|
||||||
|
)
|
||||||
|
|
||||||
|
run_id="$(date -u +%Y%m%d%H%M%S)-$$"
|
||||||
|
compact_id=$(printf '%s' "$run_id" | tr -d '-')
|
||||||
|
project="who_need_help_upgrade_rehearsal_$compact_id"
|
||||||
|
rehearsal_db="wnh_upgrade_rehearsal_$compact_id"
|
||||||
|
output_dir="$ROOT/output/upgrade-rehearsal/$run_id"
|
||||||
|
|
||||||
|
case "$project:$rehearsal_db" in
|
||||||
|
who_need_help_upgrade_rehearsal_*:wnh_upgrade_rehearsal_*) ;;
|
||||||
|
*)
|
||||||
|
echo "Refusing unexpected rehearsal identifiers." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mkdir -p "$output_dir"
|
||||||
|
chmod 700 "$ROOT/output" "$ROOT/output/upgrade-rehearsal" "$output_dir"
|
||||||
|
|
||||||
|
case "$DATABASE_URL" in
|
||||||
|
*\?*)
|
||||||
|
database_base=${DATABASE_URL%%\?*}
|
||||||
|
database_query="?${DATABASE_URL#*\?}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
database_base=$DATABASE_URL
|
||||||
|
database_query=
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
database_prefix=${database_base%/*}
|
||||||
|
|
||||||
|
if [ "$database_prefix" = "$database_base" ]; then
|
||||||
|
echo "Could not derive the isolated rehearsal DATABASE_URL." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DATABASE_URL="$database_prefix/$rehearsal_db$database_query"
|
||||||
|
export HTTP_PORT=0
|
||||||
|
export MAILPIT_PORT=0
|
||||||
|
export PHX_HOST="$rehearsal_phx_host"
|
||||||
|
export PHX_SCHEME="$rehearsal_phx_scheme"
|
||||||
|
export PHX_URL_PORT="$rehearsal_phx_url_port"
|
||||||
|
export MAP_TILE_URL="$rehearsal_map_tile_url"
|
||||||
|
export TRAEFIK_TRUSTED_IPS=127.0.0.1/32
|
||||||
|
export TRAEFIK_PROJECT_CONSTRAINT="$project"
|
||||||
|
export TRAEFIK_APP_NAME="wnh-upgrade-rehearsal-$compact_id"
|
||||||
|
export TRAEFIK_DOCKER_NETWORK="${project}_internal"
|
||||||
|
export REHEARSAL_IMAGE="who-need-help:upgrade-rehearsal-$run_id"
|
||||||
|
|
||||||
|
compose() {
|
||||||
|
docker compose \
|
||||||
|
--project-name "$project" \
|
||||||
|
--env-file "$ENV_FILE" \
|
||||||
|
--file "$ROOT/compose.yaml" \
|
||||||
|
--file "$ROOT/compose.upgrade-rehearsal.yaml" \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
status=$?
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
|
||||||
|
if [ "$status" -ne 0 ]; then
|
||||||
|
compose ps --all >"$output_dir/compose-ps.txt" 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! compose down --volumes --remove-orphans >/dev/null 2>&1; then
|
||||||
|
echo "Could not remove the isolated rehearsal Compose project." >&2
|
||||||
|
if [ "$status" -eq 0 ]; then
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! docker image rm "$REHEARSAL_IMAGE" >/dev/null 2>&1; then
|
||||||
|
if docker image inspect "$REHEARSAL_IMAGE" >/dev/null 2>&1; then
|
||||||
|
echo "Could not remove the isolated rehearsal image." >&2
|
||||||
|
if [ "$status" -eq 0 ]; then
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
remaining_containers=$(
|
||||||
|
docker ps --all --quiet \
|
||||||
|
--filter "label=com.docker.compose.project=$project"
|
||||||
|
)
|
||||||
|
remaining_volumes=$(
|
||||||
|
docker volume ls --quiet \
|
||||||
|
--filter "label=com.docker.compose.project=$project"
|
||||||
|
)
|
||||||
|
remaining_networks=$(
|
||||||
|
docker network ls --quiet \
|
||||||
|
--filter "label=com.docker.compose.project=$project"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -n "$remaining_containers$remaining_volumes$remaining_networks" ]; then
|
||||||
|
echo "Isolated rehearsal resources remain after cleanup." >&2
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
database_snapshot() {
|
||||||
|
target=$1
|
||||||
|
tables=$(
|
||||||
|
compose exec -T db \
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$rehearsal_db" \
|
||||||
|
--tuples-only --no-align \
|
||||||
|
--command "
|
||||||
|
SELECT tablename
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
AND tablename NOT IN ('oban_jobs', 'schema_migrations', 'spatial_ref_sys')
|
||||||
|
ORDER BY tablename;
|
||||||
|
"
|
||||||
|
)
|
||||||
|
|
||||||
|
: >"$target"
|
||||||
|
|
||||||
|
for table in $tables; do
|
||||||
|
row_count=$(
|
||||||
|
compose exec -T db \
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$rehearsal_db" \
|
||||||
|
--tuples-only --no-align \
|
||||||
|
--command "SELECT count(*) FROM \"$table\";"
|
||||||
|
)
|
||||||
|
printf '%s %s\n' "$table" "$row_count" >>"$target"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
compose config --quiet
|
||||||
|
compose build migrate >"$output_dir/image-build.txt"
|
||||||
|
compose up --detach --wait db
|
||||||
|
|
||||||
|
compose exec -T db \
|
||||||
|
createdb \
|
||||||
|
--username "$POSTGRES_USER" \
|
||||||
|
--template template0 \
|
||||||
|
"$rehearsal_db"
|
||||||
|
|
||||||
|
compose exec -T db \
|
||||||
|
pg_restore \
|
||||||
|
--username "$POSTGRES_USER" \
|
||||||
|
--dbname "$rehearsal_db" \
|
||||||
|
--exit-on-error \
|
||||||
|
--no-owner \
|
||||||
|
--no-privileges <"$dump"
|
||||||
|
|
||||||
|
compose exec -T db pg_restore --list <"$dump" >"$output_dir/pg-restore-list.txt"
|
||||||
|
|
||||||
|
database_snapshot "$output_dir/database-before.txt"
|
||||||
|
compose exec -T db \
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$rehearsal_db" \
|
||||||
|
--tuples-only --no-align \
|
||||||
|
--command "SELECT version FROM schema_migrations ORDER BY version;" \
|
||||||
|
>"$output_dir/migrations-before.txt"
|
||||||
|
|
||||||
|
compose up --detach --wait mailpit migrate proxy web worker
|
||||||
|
|
||||||
|
database_snapshot "$output_dir/database-after.txt"
|
||||||
|
diff -u "$output_dir/database-before.txt" "$output_dir/database-after.txt" \
|
||||||
|
>"$output_dir/database-diff.txt"
|
||||||
|
|
||||||
|
expected_migrations=$(
|
||||||
|
find "$ROOT/priv/repo/migrations" -type f -name '[0-9]*.exs' \
|
||||||
|
-exec basename {} \; |
|
||||||
|
sed 's/_.*//' |
|
||||||
|
sort >"$output_dir/migrations-expected.txt"
|
||||||
|
wc -l <"$output_dir/migrations-expected.txt" | tr -d ' '
|
||||||
|
)
|
||||||
|
actual_migrations=$(
|
||||||
|
compose exec -T db \
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$rehearsal_db" \
|
||||||
|
--tuples-only --no-align \
|
||||||
|
--command "SELECT count(*) FROM schema_migrations;"
|
||||||
|
)
|
||||||
|
compose exec -T db \
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$rehearsal_db" \
|
||||||
|
--tuples-only --no-align \
|
||||||
|
--command "SELECT version FROM schema_migrations ORDER BY version;" \
|
||||||
|
>"$output_dir/migrations-after.txt"
|
||||||
|
diff -u "$output_dir/migrations-expected.txt" "$output_dir/migrations-after.txt" \
|
||||||
|
>"$output_dir/migrations-diff.txt"
|
||||||
|
|
||||||
|
description_columns=$(
|
||||||
|
compose exec -T db \
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$rehearsal_db" \
|
||||||
|
--tuples-only --no-align \
|
||||||
|
--command "
|
||||||
|
SELECT count(*)
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'categories'
|
||||||
|
AND column_name = 'descriptions'
|
||||||
|
AND is_nullable = 'NO';
|
||||||
|
"
|
||||||
|
)
|
||||||
|
valid_cursor_indexes=$(
|
||||||
|
compose exec -T db \
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$rehearsal_db" \
|
||||||
|
--tuples-only --no-align \
|
||||||
|
--command "
|
||||||
|
SELECT count(*)
|
||||||
|
FROM pg_class index_relation
|
||||||
|
JOIN pg_index index_metadata
|
||||||
|
ON index_metadata.indexrelid = index_relation.oid
|
||||||
|
JOIN pg_namespace namespace
|
||||||
|
ON namespace.oid = index_relation.relnamespace
|
||||||
|
WHERE namespace.nspname = 'public'
|
||||||
|
AND index_relation.relname IN (
|
||||||
|
'help_requests_discovery_cursor_index',
|
||||||
|
'help_requests_requester_cursor_index',
|
||||||
|
'activities_discovery_cursor_index',
|
||||||
|
'messages_assignment_cursor_index',
|
||||||
|
'activity_messages_activity_cursor_index',
|
||||||
|
'reviews_visible_cursor_index',
|
||||||
|
'reports_cursor_index',
|
||||||
|
'blocks_blocker_cursor_index',
|
||||||
|
'abuse_signals_status_cursor_index',
|
||||||
|
'category_proposals_cursor_index',
|
||||||
|
'users_moderation_cursor_index'
|
||||||
|
)
|
||||||
|
AND index_metadata.indisvalid
|
||||||
|
AND index_metadata.indisready;
|
||||||
|
"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "$actual_migrations" != "$expected_migrations" ]; then
|
||||||
|
echo "Rehearsal migration count is $actual_migrations; expected $expected_migrations." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$description_columns" != "1" ] || [ "$valid_cursor_indexes" != "11" ]; then
|
||||||
|
echo "The migrated rehearsal schema is incomplete." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
proxy_port=$(compose port proxy 80 | sed 's/.*://')
|
||||||
|
|
||||||
|
case "$proxy_port" in
|
||||||
|
'' | *[!0-9]*)
|
||||||
|
echo "Could not determine the isolated proxy port." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
web_container=$(compose ps --quiet web | head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$web_container" ]; then
|
||||||
|
echo "No rehearsal web replica was found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$PHX_SCHEME:$PHX_URL_PORT" in
|
||||||
|
http:80 | https:443)
|
||||||
|
public_origin="$PHX_SCHEME://$PHX_HOST"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
public_origin="$PHX_SCHEME://$PHX_HOST:$PHX_URL_PORT"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
{
|
||||||
|
for path in / /safety; do
|
||||||
|
header_file="$output_dir/redirect-$(printf '%s' "$path" | tr '/' '_').txt"
|
||||||
|
status_code=$(
|
||||||
|
curl \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--dump-header "$header_file" \
|
||||||
|
--output /dev/null \
|
||||||
|
--write-out '%{http_code}' \
|
||||||
|
--header "Host: $PHX_HOST" \
|
||||||
|
"http://127.0.0.1:$proxy_port$path"
|
||||||
|
)
|
||||||
|
|
||||||
|
location=$(
|
||||||
|
awk 'tolower($1) == "location:" {sub(/\r$/, "", $2); print $2}' \
|
||||||
|
"$header_file"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "$status_code" != "301" ] || [ "$location" != "$public_origin$path" ]; then
|
||||||
|
echo "Unexpected production redirect for $path: HTTP $status_code $location." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'proxy %s %s %s\n' "$path" "$status_code" "$location"
|
||||||
|
done
|
||||||
|
|
||||||
|
for path in /healthz/live /healthz/ready; do
|
||||||
|
status_code=$(
|
||||||
|
curl \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--output /dev/null \
|
||||||
|
--write-out '%{http_code}' \
|
||||||
|
--header "Host: $PHX_HOST" \
|
||||||
|
"http://127.0.0.1:$proxy_port$path"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "$status_code" != "200" ]; then
|
||||||
|
echo "Unexpected HTTP $status_code for $path." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'proxy %s %s\n' "$path" "$status_code"
|
||||||
|
done
|
||||||
|
|
||||||
|
for path in / /safety; do
|
||||||
|
status_code=$(
|
||||||
|
docker exec "$web_container" \
|
||||||
|
curl \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--output /dev/null \
|
||||||
|
--write-out '%{http_code}' \
|
||||||
|
--header "Host: $PHX_HOST" \
|
||||||
|
--header "X-Forwarded-Proto: $PHX_SCHEME" \
|
||||||
|
--header "X-Forwarded-Port: $PHX_URL_PORT" \
|
||||||
|
"http://127.0.0.1:4000$path"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "$status_code" != "200" ]; then
|
||||||
|
echo "Unexpected trusted-proxy HTTP $status_code for $path." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'trusted-proxy %s %s\n' "$path" "$status_code"
|
||||||
|
done
|
||||||
|
} >"$output_dir/http.txt"
|
||||||
|
|
||||||
|
docker exec "$web_container" /app/bin/who_need_help rpc '
|
||||||
|
nodes = Enum.sort([node() | Node.list()])
|
||||||
|
IO.inspect(%{nodes: nodes, count: length(nodes)})
|
||||||
|
if length(nodes) != 4, do: exit({:unexpected_cluster_size, nodes})
|
||||||
|
' >"$output_dir/cluster.txt"
|
||||||
|
|
||||||
|
COMPOSE_PROJECT_NAME="$project" \
|
||||||
|
"$ROOT/scripts/verify-realtime-cluster.sh" compose \
|
||||||
|
>"$output_dir/pubsub.txt"
|
||||||
|
|
||||||
|
image_id=$(docker image inspect --format '{{.Id}}' "$REHEARSAL_IMAGE")
|
||||||
|
source_commit=$(git rev-parse HEAD)
|
||||||
|
|
||||||
|
cat >"$output_dir/summary.json" <<EOF
|
||||||
|
{
|
||||||
|
"status": "passed",
|
||||||
|
"source_commit": "$source_commit",
|
||||||
|
"image_id": "$image_id",
|
||||||
|
"input_dump": "$dump_name",
|
||||||
|
"expected_migrations": $expected_migrations,
|
||||||
|
"actual_migrations": $actual_migrations,
|
||||||
|
"migration_version_diff_bytes": 0,
|
||||||
|
"valid_cursor_indexes": $valid_cursor_indexes,
|
||||||
|
"application_table_diff_bytes": 0,
|
||||||
|
"web_replicas": 2,
|
||||||
|
"worker_replicas": 2,
|
||||||
|
"cluster_nodes": 4,
|
||||||
|
"pubsub": "passed",
|
||||||
|
"http": "passed"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 600 "$output_dir"/*
|
||||||
|
|
||||||
|
echo "Isolated Compose upgrade rehearsal passed."
|
||||||
|
echo "Evidence: $output_dir"
|
||||||
Loading…
Reference in New Issue
Block a user