#!/usr/bin/env bash # TDD verification script for api/Dockerfile.prod # Fails (red) if Dockerfile.prod does not exist or any check fails. set -euo pipefail IMAGE="reactbin-api-prod:verify-$$" IMAGE2="reactbin-api-prod:verify-cache-$$" PG_CONTAINER="" APP_CONTAINER="" cleanup() { [ -n "$APP_CONTAINER" ] && docker rm -f "$APP_CONTAINER" 2>/dev/null || true [ -n "$PG_CONTAINER" ] && docker rm -f "$PG_CONTAINER" 2>/dev/null || true docker rmi "$IMAGE" 2>/dev/null || true docker rmi "$IMAGE2" 2>/dev/null || true } trap cleanup EXIT # ── US1 check 1: build ──────────────────────────────────────────────────────── echo "[verify] Building $IMAGE..." docker build -f api/Dockerfile.prod api/ -t "$IMAGE" echo "[verify] Build OK" # ── US1 check 2: start with a throwaway postgres ────────────────────────────── echo "[verify] Starting postgres..." PG_CONTAINER=$(docker run -d \ -e POSTGRES_DB=reactbin_verify \ -e POSTGRES_USER=verify \ -e POSTGRES_PASSWORD=verify \ postgres:16-alpine) for i in $(seq 1 30); do if docker exec "$PG_CONTAINER" pg_isready -U verify -q 2>/dev/null; then break; fi sleep 1 if [[ $i -eq 30 ]]; then echo "FAIL: postgres did not become ready"; exit 1; fi done PG_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG_CONTAINER") echo "[verify] Starting production container..." APP_CONTAINER=$(docker run -d \ -p 18000:8000 \ -e JWT_SECRET_KEY=verify-key \ -e OWNER_USERNAME=testowner \ -e OWNER_PASSWORD=testpassword \ -e DATABASE_URL="postgresql+asyncpg://verify:verify@${PG_IP}:5432/reactbin_verify" \ -e S3_ENDPOINT_URL=http://noop:9000 \ -e S3_BUCKET_NAME=noop \ -e S3_ACCESS_KEY_ID=noop \ -e S3_SECRET_ACCESS_KEY=noop \ -e S3_REGION=us-east-1 \ "$IMAGE") # ── US1 check 3: health endpoint ────────────────────────────────────────────── echo "[verify] Polling health endpoint..." for i in $(seq 1 30); do if curl -sf http://localhost:18000/api/v1/health > /dev/null; then break; fi sleep 1 if [[ $i -eq 30 ]]; then echo "FAIL: health check timed out after 30s"; exit 1; fi done echo "[verify] Health check passed" # ── US2 check 1: non-root user ──────────────────────────────────────────────── UID_IN_CONTAINER=$(docker exec "$APP_CONTAINER" id -u) if [[ "$UID_IN_CONTAINER" -eq 0 ]]; then echo "FAIL: process running as root (UID 0)"; exit 1 fi echo "[verify] Non-root user OK (UID $UID_IN_CONTAINER)" # ── C1: stdout/stderr log capture ──────────────────────────────────────────── LOGS=$(docker logs "$APP_CONTAINER" 2>&1) if [[ -z "$LOGS" ]]; then echo "FAIL: no output on stdout/stderr"; exit 1 fi if ! echo "$LOGS" | grep -qiE "(started server|application startup complete|uvicorn)"; then echo "FAIL: no startup logs found on stdout/stderr"; exit 1 fi echo "[verify] Stdout logging OK" # ── US1 check 4: SIGTERM → exit 0 ──────────────────────────────────────────── docker stop "$APP_CONTAINER" > /dev/null EXIT_CODE=$(docker wait "$APP_CONTAINER") if [[ "$EXIT_CODE" -ne 0 ]]; then echo "FAIL: non-zero exit code $EXIT_CODE after SIGTERM"; exit 1 fi echo "[verify] Graceful shutdown OK (exit $EXIT_CODE)" # ── US2 check 2: dev deps absent ───────────────────────────────────────────── if docker run --rm "$IMAGE" /app/.venv/bin/python -c "import pytest" 2>/dev/null; then echo "FAIL: pytest importable in production image (dev deps present)"; exit 1 fi echo "[verify] Dev deps absent OK" # ── C2: no hardcoded secrets in image layers ───────────────────────────────── if docker history --no-trunc "$IMAGE" 2>&1 | grep -qiE "(password|secret_key|api_key|token)"; then echo "FAIL: potential secret found in image history"; exit 1 fi echo "[verify] No secrets in image layers OK" # ── C3: missing env var → non-zero exit ────────────────────────────────────── set +e docker run --rm -e JWT_SECRET_KEY=verify-key "$IMAGE" 2>/dev/null MISSING_ENV_EXIT=$? set -e if [[ "$MISSING_ENV_EXIT" -eq 0 ]]; then echo "FAIL: container exited 0 despite missing OWNER_USERNAME"; exit 1 fi echo "[verify] Missing-env-var exit check OK (exit $MISSING_ENV_EXIT)" # ── US3: dep layer cached on source-only rebuild ────────────────────────────── echo "[verify] Testing cache hit on source-only rebuild..." touch api/app/main.py BUILD2_OUTPUT=$(docker build --progress=plain -f api/Dockerfile.prod api/ -t "$IMAGE2" 2>&1) if ! echo "$BUILD2_OUTPUT" | grep -q "CACHED"; then echo "FAIL: dependency layer not reused on source-only rebuild"; exit 1 fi echo "[verify] Dep layer cache hit confirmed (US3 OK)" echo "[verify] All checks passed (US1 + US2 + US3)."