Skip to content

Commit 61ab15d

Browse files
committed
feat(dev): add local api test runner
1 parent 6a4ae93 commit 61ab15d

2 files changed

Lines changed: 149 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ api/venv
77

88
# development helper scripts
99
/*.sh
10+
/.cache/
1011

1112
# local certificates
1213
/certs/

run-api-tests-stack.sh

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
COMPOSE="docker compose -f ${ROOT_DIR}/docker-compose.yml -f ${ROOT_DIR}/docker-compose.test-api.yml"
6+
BUILD_IMAGES_DB=(dbapi)
7+
BUILD=1
8+
PROD_DB=0
9+
PROD_USER="root"
10+
PROD_HOST="digga.desec.io"
11+
PROD_REFRESH=0
12+
CACHE_DIR="${ROOT_DIR}/.cache/prod-db"
13+
CACHE_FILE="${CACHE_DIR}/dbapi.sql.gz"
14+
15+
usage() {
16+
cat <<'EOF'
17+
Usage: ./run-api-tests-stack.sh [--no-build] [--prod-db] [--prod-user USER] [--prod-host HOST] [--refresh-prod-db]
18+
--no-build Skip docker image build step
19+
--prod-db Download a logical dbapi dump from production and load it locally
20+
--prod-user SSH username for prod (default: root)
21+
--prod-host SSH hostname for prod (default: desec.io)
22+
--refresh-prod-db Re-download prod db dump even if cache exists
23+
EOF
24+
}
25+
26+
while [[ $# -gt 0 ]]; do
27+
case "$1" in
28+
--no-build) BUILD=0 ;;
29+
--prod-db) PROD_DB=1 ;;
30+
--prod-user)
31+
shift
32+
PROD_USER="${1:-}"
33+
[[ -n "$PROD_USER" ]] || { echo "Missing value for --prod-user" >&2; exit 1; }
34+
;;
35+
--prod-host)
36+
shift
37+
PROD_HOST="${1:-}"
38+
[[ -n "$PROD_HOST" ]] || { echo "Missing value for --prod-host" >&2; exit 1; }
39+
;;
40+
--refresh-prod-db) PROD_REFRESH=1 ;;
41+
-h|--help) usage; exit 0 ;;
42+
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
43+
esac
44+
shift
45+
done
46+
47+
prod_remote_script() {
48+
cat <<'EOF'
49+
set -euo pipefail
50+
cd /root/desec-stack
51+
docker compose -f docker-compose.yml exec -T dbapi pg_dump -Fc -U desec desec | gzip -c
52+
EOF
53+
}
54+
55+
download_prod_dbapi() {
56+
mkdir -p "$CACHE_DIR"
57+
if [[ "$PROD_REFRESH" -eq 0 && -f "$CACHE_FILE" ]]; then
58+
echo "Using cached prod db dump at ${CACHE_FILE}"
59+
return
60+
fi
61+
62+
local prod_ssh="${PROD_USER}@${PROD_HOST}"
63+
echo "About to run the following read-only commands on ${prod_ssh}:"
64+
prod_remote_script
65+
read -r -p "Continue? [y/N] " reply
66+
case "$reply" in
67+
y|Y|yes|YES) ;;
68+
*) echo "Aborted." >&2; exit 1 ;;
69+
esac
70+
71+
local tmp_file
72+
local old_umask
73+
old_umask="$(umask)"
74+
umask 077
75+
tmp_file="$(mktemp "${CACHE_FILE}.tmp.XXXXXX")"
76+
umask "$old_umask"
77+
if prod_remote_script | ssh -4 "$prod_ssh" "bash -s" > "$tmp_file"; then
78+
mv "$tmp_file" "$CACHE_FILE"
79+
echo "Saved prod db dump to ${CACHE_FILE}"
80+
else
81+
rm -f "$tmp_file"
82+
echo "Failed to download prod db dump; cache not updated." >&2
83+
exit 1
84+
fi
85+
}
86+
87+
restore_dbapi_from_cache() {
88+
if [[ ! -f "$CACHE_FILE" ]]; then
89+
echo "Missing cache file: ${CACHE_FILE}" >&2
90+
exit 1
91+
fi
92+
$COMPOSE exec -T dbapi sh -c "psql -U postgres -d postgres -c \"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='desec' AND pid <> pg_backend_pid();\""
93+
$COMPOSE exec -T dbapi sh -c "psql -U postgres -d postgres -c \"DROP DATABASE IF EXISTS desec;\""
94+
$COMPOSE exec -T dbapi sh -c "psql -U postgres -d postgres -c \"CREATE DATABASE desec OWNER desec;\""
95+
gunzip -c "$CACHE_FILE" | $COMPOSE exec -T dbapi sh -c "pg_restore -U desec -d desec --no-owner --no-acl --role=desec"
96+
}
97+
98+
cleanup() {
99+
$COMPOSE ps || true
100+
$COMPOSE down || true
101+
}
102+
103+
trap cleanup EXIT
104+
105+
if [[ "$BUILD" -eq 1 ]]; then
106+
$COMPOSE build "${BUILD_IMAGES_DB[@]}"
107+
fi
108+
$COMPOSE up -d dbapi
109+
(
110+
set -a
111+
source "${ROOT_DIR}/.env"
112+
set +a
113+
export DJANGO_SETTINGS_MODULE=api.settings_quick_test
114+
export DESECSTACK_DJANGO_TEST=1
115+
cd "${ROOT_DIR}/api"
116+
if [[ -x "./venv/bin/python" ]]; then
117+
# Use project venv when present.
118+
source "./venv/bin/activate"
119+
else
120+
echo "Missing venv at ${ROOT_DIR}/api/venv. Create it with: cd api && python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt" >&2
121+
exit 1
122+
fi
123+
# Avoid local psql dependency by checking readiness inside the DB container.
124+
wait_seconds=120
125+
start_ts=$(date +%s)
126+
while true; do
127+
if $COMPOSE exec -T dbapi pg_isready >/dev/null 2>&1; then
128+
break
129+
fi
130+
131+
now_ts=$(date +%s)
132+
if (( now_ts - start_ts > wait_seconds )); then
133+
echo "Timed out waiting for Postgres to become ready."
134+
$COMPOSE ps
135+
$COMPOSE logs --tail 80 dbapi || true
136+
exit 1
137+
fi
138+
echo "Postgres is unavailable - sleeping"
139+
sleep 2
140+
done
141+
test_args=()
142+
if [[ "$PROD_DB" -eq 1 ]]; then
143+
download_prod_dbapi
144+
restore_dbapi_from_cache
145+
test_args+=(--keepdb)
146+
fi
147+
python3 manage.py test "${test_args[@]}"
148+
)

0 commit comments

Comments
 (0)