Files
awg-dash/deploy/verify-dev-stand.sh

218 lines
9.8 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# verify-dev-stand.sh — проверка acceptance criteria задачи #21 на Docker-хосте.
#
# Запускать из корня репозитория на машине с Docker:
# bash deploy/verify-dev-stand.sh # реальный API (после мержа #31)
# bash deploy/verify-dev-stand.sh --stub # проверка плумбинга до #31 (health-заглушка)
#
# Что проверяет:
# AC-1: `curl http://<host>:<port>/api/v1/health` -> 200;
# AC-2: правки в /config и /data переживают `docker compose restart`
# (и дополнительно пересоздание контейнера);
# пласт: монтирования /config и /data указывают на ожидаемый каталог хоста,
# контейнер healthy, образ и compose-файлы на месте.
#
# Коды возврата:
# 0 — AC-1 и AC-2 выполнены;
# 1 — AC нарушены (регрессия стенда);
# 2 — AC-1 заблокирован: API ещё не реализован (#31), стенд при этом поднят;
# 3 — заблокировано окружением (нет docker/compose/файлов).
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_DEV="$ROOT/deploy/docker-compose.dev.yml"
COMPOSE_STUB="$ROOT/deploy/docker-compose.stub.yml"
STUB=0
DO_UP=1
DO_BUILD=1
KEEP_STAND=0
HOST="127.0.0.1"
PORT="${AWG_DASH_PORT:-8180}"
HOST_DIR="${AWG_DASH_HOST_DIR:-/opt/awg-dash-dev}"
JSON_OUT=""
HEALTH_PATH="/api/v1/health"
usage() { sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
while [ $# -gt 0 ]; do
case "$1" in
--stub) STUB=1 ;;
--no-up) DO_UP=0 ;;
--skip-build) DO_BUILD=0 ;;
--keep) KEEP_STAND=1 ;;
--host) HOST="${2:?}"; shift ;;
--port) PORT="${2:?}"; shift ;;
--host-dir) HOST_DIR="${2:?}"; shift ;;
--compose) COMPOSE_DEV="${2:?}"; shift ;;
--json) JSON_OUT="${2:?}"; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "неизвестная опция: $1" >&2; exit 3 ;;
esac
shift
done
fatal_env() { printf '[verify][blocked] %s\n' "$*" >&2; exit 3; }
say() { printf '[verify] %s\n' "$*"; }
ok() { printf '[verify][ ok ] %s\n' "$*"; }
bad() { printf '[verify][fail] %s\n' "$*" >&2; }
# --- предусловия -------------------------------------------------------------
command -v docker >/dev/null 2>&1 || fatal_env "нет docker в PATH"
docker compose version >/dev/null 2>&1 || fatal_env "нет плагина 'docker compose'"
[ -f "$COMPOSE_DEV" ] || fatal_env "нет файла $COMPOSE_DEV"
COMPOSE_ARGS=(-f "$COMPOSE_DEV")
if [ "$STUB" -eq 1 ]; then
[ -f "$COMPOSE_STUB" ] || fatal_env "нет файла $COMPOSE_STUB"
COMPOSE_ARGS+=(-f "$COMPOSE_STUB")
fi
compose() { docker compose "${COMPOSE_ARGS[@]}" "$@"; }
AC1="not_run"
AC2="not_run"
API_MODE="unknown"
NOTES=()
FAILURES=0
# --- поднятие стенда ---------------------------------------------------------
if [ "$DO_UP" -eq 1 ]; then
UP_ARGS=(up -d)
[ "$DO_BUILD" -eq 1 ] && UP_ARGS+=(--build)
say "docker compose ${COMPOSE_ARGS[*]} ${UP_ARGS[*]}"
compose "${UP_ARGS[@]}" || fatal_env "не удалось поднять dev-стенд (см. вывод docker compose)"
fi
compose ps >/dev/null 2>&1 || fatal_env "compose project не поднят"
cid() { compose ps -q awg-dash | head -n 1; }
CID="$(cid)"
[ -n "$CID" ] || fatal_env "контейнер awg-dash не найден в compose project"
wait_health() {
local tries="${1:-30}" code i
for ((i = 1; i <= tries; i++)); do
code="$(curl -s -o /dev/null -m 3 -w '%{http_code}' "http://${HOST}:${PORT}${HEALTH_PATH}" || true)"
[ "$code" = "200" ] && return 0
sleep 2
done
return 1
}
# --- AC-1: health 200 --------------------------------------------------------
BODY="$(curl -s -m 3 "http://${HOST}:${PORT}${HEALTH_PATH}" || true)"
if wait_health 20; then
BODY="$(curl -s -m 3 "http://${HOST}:${PORT}${HEALTH_PATH}" || true)"
if printf '%s' "$BODY" | grep -q '"stub": *true'; then
API_MODE="stub"
NOTES+=("health отвечает заглушка (#31): реальный API ещё не смёржен")
ok "AC-1: http://${HOST}:${PORT}${HEALTH_PATH} -> 200 (api=stub)"
else
API_MODE="real"
ok "AC-1: http://${HOST}:${PORT}${HEALTH_PATH} -> 200 (api=real)"
fi
AC1="pass"
else
CODE="$(curl -s -o /dev/null -m 3 -w '%{http_code}' "http://${HOST}:${PORT}${HEALTH_PATH}" || echo "000")"
say "health-check вернул ${CODE}; последние логи контейнера:"
compose logs --tail=15 awg-dash || true
if [ "$STUB" -eq 0 ]; then
AC1="blocked"
NOTES+=("AC-1 заблокирован: FastAPI-каркас (#31) ещё не реализован — /api/v1/health не отвечает")
printf '[verify][blocked] AC-1 требует #31 (см. deploy/dev-stand.md)\n' >&2
else
AC1="fail"
FAILURES=$((FAILURES + 1))
bad "AC-1: даже с заглушкой health не 200 (код ${CODE})"
fi
fi
# --- AC-2: состояние переживает restart --------------------------------------
if [ "$AC1" = "blocked" ]; then
NOTES+=("AC-2 проверяем без API: маркерные файлы в /config и /data")
fi
STAMP="$(date +%s)"
MARKER_TEXT="awg-dash-ac2-${STAMP}"
say "AC-2: пишу маркеры в /config и /data и перезапускаю контейнер"
if compose exec -T awg-dash sh -c "printf '%s\n' '$MARKER_TEXT' > /config/.ac2-marker && printf '%s\n' '$MARKER_TEXT' > /data/.ac2-marker"; then
# 1) restart (как в AC)
compose restart awg-dash >/dev/null
if [ "$AC1" = "pass" ]; then wait_health 20 || NOTES+=("после restart health не поднялся за 40с"); fi
CFG_AFTER="$(compose exec -T awg-dash cat /config/.ac2-marker 2>/dev/null | tr -d '\r\n' || true)"
DATA_AFTER="$(compose exec -T awg-dash cat /data/.ac2-marker 2>/dev/null | tr -d '\r\n' || true)"
# 2) пересоздание контейнера (сильнее, чем restart)
compose up -d --force-recreate >/dev/null
if [ "$AC1" = "pass" ]; then wait_health 20 || NOTES+=("после пересоздания health не поднялся за 40с"); fi
# контейнер пересоздан — id сменился; без перезапроса docker inspect падает с 'no such object'
CID="$(cid)"
[ -n "$CID" ] || fatal_env "контейнер awg-dash не найден после пересоздания"
CFG_RECREATE="$(compose exec -T awg-dash cat /config/.ac2-marker 2>/dev/null | tr -d '\r\n' || true)"
DATA_RECREATE="$(compose exec -T awg-dash cat /data/.ac2-marker 2>/dev/null | tr -d '\r\n' || true)"
HOST_CFG="$(tr -d '\r\n' < "${HOST_DIR}/config/.ac2-marker" 2>/dev/null || true)"
HOST_DATA="$(tr -d '\r\n' < "${HOST_DIR}/data/.ac2-marker" 2>/dev/null || true)"
if [ "$CFG_AFTER" = "$MARKER_TEXT" ] && [ "$DATA_AFTER" = "$MARKER_TEXT" ] \
&& [ "$CFG_RECREATE" = "$MARKER_TEXT" ] && [ "$DATA_RECREATE" = "$MARKER_TEXT" ]; then
AC2="pass"
ok "AC-2: /config и /data пережили restart и пересоздание контейнера"
if [ "$HOST_CFG" = "$MARKER_TEXT" ] && [ "$HOST_DATA" = "$MARKER_TEXT" ]; then
ok "AC-2: маркеры видны на хосте (${HOST_DIR}/{config,data}) — это bind mount, а не слой контейнера"
else
NOTES+=("маркеры не видны в ${HOST_DIR} — проверьте, что это bind mount, а не volume")
fi
else
AC2="fail"
FAILURES=$((FAILURES + 1))
bad "AC-2: маркеры не пережили restart/пересоздание (cfg=$CFG_AFTER data=$DATA_AFTER recreate=$CFG_RECREATE/$DATA_RECREATE)"
fi
else
AC2="fail"
FAILURES=$((FAILURES + 1))
bad "AC-2: не удалось записать маркеры в /config и /data (exec в контейнер не работает)"
fi
# --- монтирования и здоровье -------------------------------------------------
CID="$(cid)"
[ -n "$CID" ] || fatal_env "контейнер awg-dash не найден в compose project (после перезапуска)"
MOUNTS="$(docker inspect -f '{{range .Mounts}}{{.Destination}}<-{{.Source}}{{"\n"}}{{end}}' "$CID" 2>/dev/null || true)"
printf '%s\n' "$MOUNTS" | grep -q '/config' || { FAILURES=$((FAILURES + 1)); bad "нет монтирования /config"; }
printf '%s\n' "$MOUNTS" | grep -q '/data' || { FAILURES=$((FAILURES + 1)); bad "нет монтирования /data"; }
HEALTH_STATUS="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$CID" 2>/dev/null || echo unknown)"
say "контейнер: $(docker inspect -f '{{.Name}} {{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$CID")"
say "монтирования:"; printf '%s\n' "$MOUNTS" | sed 's/^/ /'
say "образ: $(docker inspect -f '{{.Config.Image}}' "$CID")"
if [ "$JSON_OUT" != "" ]; then
mkdir -p "$(dirname "$JSON_OUT")"
cat > "$JSON_OUT" <<EOF
{
"task": "#21",
"host": "$(hostname)",
"container": "$CID",
"image": "$(docker inspect -f '{{.Config.Image}}' "$CID")",
"api_mode": "$API_MODE",
"ac1_health_200": "$AC1",
"ac2_state_persists_restart": "$AC2",
"container_health": "$HEALTH_STATUS",
"host_dir": "${HOST_DIR}",
"host_port": "${PORT}",
"failures": ${FAILURES},
"notes": [$(printf '"%s",' "${NOTES[@]:-}" 2>/dev/null | sed 's/,$//')]
}
EOF
ok "отчёт: ${JSON_OUT}"
fi
if [ "$KEEP_STAND" -eq 0 ]; then
say "стенд оставлен запущенным (снять: docker compose ${COMPOSE_ARGS[*]} down)"
fi
if [ "$FAILURES" -gt 0 ]; then
exit 1
fi
[ "$AC1" = "blocked" ] && exit 2
exit 0