#!/usr/bin/env bash # Smoke-тест собранного образа: запуск с боевыми ограничениями (как в compose), # /ready, логин, JWKS, бюджеты RSS/размера, время docker stop, сохранность данных. # # scripts/ci/smoke-image.sh # # Запросы идут ВНУТРИ контейнера (docker exec + busybox wget): раннер CI сам работает # в контейнере, и опубликованный порт хоста с 127.0.0.1 задания недостижим. # # Бюджеты (env): MAX_IMAGE_MB (250), MAX_RSS_MB (200), MAX_START_S (15), MAX_STOP_S (5) set -euo pipefail IMAGE="${1:?usage: smoke-image.sh }" MAX_IMAGE_MB="${MAX_IMAGE_MB:-250}" MAX_RSS_MB="${MAX_RSS_MB:-200}" MAX_START_S="${MAX_START_S:-15}" MAX_STOP_S="${MAX_STOP_S:-5}" NAME="authportal-smoke-$$" VOLUME="authportal-smoke-data-$$" SECRET="smoke-jwt-secret-0123456789" ADMIN_PW="smoke-admin-pass-1" BASE="http://127.0.0.1:8080" fail() { echo "SMOKE FAIL: $*" >&2; docker logs --tail 50 "$NAME" >&2 || true; exit 1; } cleanup() { docker rm -f "$NAME" >/dev/null 2>&1 || true docker volume rm -f "$VOLUME" >/dev/null 2>&1 || true } trap cleanup EXIT # GET (или POST с JSON-телом) изнутри контейнера; тело ответа в stdout. req() { local url="$1" body="${2:-}" if [ -n "$body" ]; then docker exec "$NAME" wget -q -O - -T 5 \ --header='content-type: application/json' --post-data="$body" "$url" else docker exec "$NAME" wget -q -O - -T 5 "$url" fi } run_container() { docker run -d --name "$NAME" --init \ --memory 256m --memory-swap 256m --cpus 1.0 --pids-limit 128 \ --read-only --tmpfs /tmp --cap-drop ALL --security-opt no-new-privileges:true \ -v "$VOLUME:/data" \ -e JWT_SECRET="$SECRET" -e ADMIN_PASSWORD="$ADMIN_PW" \ -e ADMIN_EMAIL=admin@smoke.local -e ISSUER="$BASE" \ "$IMAGE" >/dev/null } wait_ready() { local start deadline start=$(date +%s) deadline=$((start + MAX_START_S)) until req "$BASE/ready" >/dev/null 2>&1; do [ "$(date +%s)" -lt "$deadline" ] || fail "/ready не ответил за ${MAX_START_S}s" sleep 0.3 done echo "ready за $(( $(date +%s) - start ))s" } login() { req "$BASE/api/v1/auth/login" \ "{\"email\":\"admin@smoke.local\",\"password\":\"${ADMIN_PW}\"}" } jwks_kid() { req "$BASE/.well-known/jwks.json" | sed -E 's/.*"kid":"([^"]+)".*/\1/' } # 1. размер образа size_mb=$(( $(docker image inspect "$IMAGE" --format '{{.Size}}') / 1024 / 1024 )) echo "image size: ${size_mb} MiB (budget ${MAX_IMAGE_MB})" [ "$size_mb" -le "$MAX_IMAGE_MB" ] || fail "образ ${size_mb} MiB > ${MAX_IMAGE_MB} MiB" # 2. старт, ready, логин, JWKS docker volume create "$VOLUME" >/dev/null docker run --rm -v "$VOLUME:/data" --user root --entrypoint sh "$IMAGE" -c 'chown 1000:1000 /data' >/dev/null 2>&1 || true run_container wait_ready [ "$(req "$BASE/health")" = '{"ok":true}' ] || fail "/health" login | grep -q '"access_token"' || fail "логин администратора" kid1=$(jwks_kid) [ -n "$kid1" ] || fail "JWKS пуст" # wget на 404 завершается с ошибкой (pipefail!) — собираем вывод отдельно. not_found=$(docker exec "$NAME" wget -S -O /dev/null -T 5 "$BASE/oauth/nope" 2>&1 || true) echo "$not_found" | grep -q "HTTP/1.[01] 404" || fail "/oauth/nope: ожидался 404, получено: $not_found" # 3. память под нагрузкой логинов for _ in $(seq 1 10); do login >/dev/null; done rss_mb=$(docker stats --no-stream --format '{{.MemUsage}}' "$NAME" \ | sed -E 's/^([0-9.]+)([KMG]i?B).*/\1 \2/' \ | awk '{u=$2; v=$1; if(u ~ /^G/) v*=1024; else if(u ~ /^K/) v/=1024; printf "%d", v}') echo "RSS: ${rss_mb} MiB (budget ${MAX_RSS_MB})" [ "$rss_mb" -le "$MAX_RSS_MB" ] || fail "RSS ${rss_mb} MiB > ${MAX_RSS_MB} MiB" # 4. быстрый graceful stop t0=$(date +%s.%N) docker stop -t 15 "$NAME" >/dev/null elapsed=$(awk -v a="$t0" -v b="$(date +%s.%N)" 'BEGIN{printf "%.1f", b-a}') echo "docker stop: ${elapsed}s (budget ${MAX_STOP_S}s)" awk -v e="$elapsed" -v m="$MAX_STOP_S" 'BEGIN{exit !(e<=m)}' || fail "docker stop ${elapsed}s > ${MAX_STOP_S}s" [ "$(docker inspect "$NAME" --format '{{.State.ExitCode}}')" = 0 ] || fail "код выхода после SIGTERM не 0" # 5. данные переживают рестарт (тот же ключ подписи и тот же админ) docker rm "$NAME" >/dev/null run_container wait_ready login | grep -q '"access_token"' || fail "логин после рестарта" kid2=$(jwks_kid) [ "$kid1" = "$kid2" ] || fail "kid изменился после рестарта ($kid1 → $kid2)" echo "SMOKE OK"