From 3fe9705782693c5d0fad8483bdfa9f9a1c5fe568 Mon Sep 17 00:00:00 2001 From: suguanYang Date: Thu, 13 Aug 2026 11:36:25 +0800 Subject: [PATCH] test: add local ECS runtime smoke test --- deploy/ecs/README.md | 10 ++ deploy/ecs/docker-runtime-smoke.sh | 151 +++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100755 deploy/ecs/docker-runtime-smoke.sh diff --git a/deploy/ecs/README.md b/deploy/ecs/README.md index 74444776..b02ebd3a 100644 --- a/deploy/ecs/README.md +++ b/deploy/ecs/README.md @@ -32,3 +32,13 @@ The staging workflow in `.github/workflows/build-images.yml` expects these GitHu - `STAGING_MIGRATION_DATABASE_URL`: the direct Neon URL for the staging migration role. It is used only by the migration container and is never passed to the ECS runtime tasks. Before an ECS staging deployment, an operator must create and verify the ECS services, network configuration, API load-balancer target, CloudWatch log groups, and runtime secret. The workflow validates those resources and fails without registering or updating a service when any prerequisite is missing. It does not create or delete AWS resources. + +## Local Docker smoke test + +LocalStack Community does not implement the ECS API used by this deployment, so it cannot validate Fargate orchestration. The runtime can still be checked locally with Docker: + +```bash +./deploy/ecs/docker-runtime-smoke.sh +``` + +The script creates an isolated Docker network, starts disposable PostgreSQL and Redis containers, builds the API and worker images, runs Alembic, and verifies the API and worker container health checks. It uses filesystem object storage and mock LLM responses, calls no AWS API, and removes its containers and network on exit. Override `POSTGRES_IMAGE` or `REDIS_IMAGE` when the official images are not available locally. When `http_proxy` or `https_proxy` is set, the script passes those values only to the Docker build process. diff --git a/deploy/ecs/docker-runtime-smoke.sh b/deploy/ecs/docker-runtime-smoke.sh new file mode 100755 index 00000000..53ff85bf --- /dev/null +++ b/deploy/ecs/docker-runtime-smoke.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash + +set -euo pipefail + +NETWORK_NAME="knowhere-runtime-smoke" +POSTGRES_CONTAINER="knowhere-runtime-smoke-postgres" +REDIS_CONTAINER="knowhere-runtime-smoke-redis" +API_CONTAINER="knowhere-runtime-smoke-api" +WORKER_CONTAINER="knowhere-runtime-smoke-worker" +API_IMAGE="${API_IMAGE:-knowhere-runtime-smoke-api:local}" +WORKER_IMAGE="${WORKER_IMAGE:-knowhere-runtime-smoke-worker:local}" +POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgres:15-alpine}" +REDIS_IMAGE="${REDIS_IMAGE:-redis:7-alpine}" + +cleanup() { + docker rm --force \ + "$API_CONTAINER" \ + "$WORKER_CONTAINER" \ + "$POSTGRES_CONTAINER" \ + "$REDIS_CONTAINER" >/dev/null 2>&1 || true + docker network rm "$NETWORK_NAME" >/dev/null 2>&1 || true +} + +trap cleanup EXIT + +wait_for_postgres() { + for _attempt in $(seq 1 60); do + if docker exec "$POSTGRES_CONTAINER" pg_isready -U root -d Knowhere >/dev/null 2>&1; then + return 0 + fi + sleep 2 + done + docker logs "$POSTGRES_CONTAINER" || true + return 1 +} + +wait_for_redis() { + for _attempt in $(seq 1 60); do + if docker exec "$REDIS_CONTAINER" redis-cli ping >/dev/null 2>&1; then + return 0 + fi + sleep 2 + done + docker logs "$REDIS_CONTAINER" || true + return 1 +} + +wait_for_api() { + for _attempt in $(seq 1 90); do + if docker exec "$API_CONTAINER" curl --fail --silent http://localhost:5005/health >/dev/null 2>&1; then + return 0 + fi + if ! docker inspect --format '{{.State.Running}}' "$API_CONTAINER" 2>/dev/null | grep --quiet true; then + docker logs "$API_CONTAINER" || true + return 1 + fi + sleep 2 + done + docker logs "$API_CONTAINER" || true + return 1 +} + +wait_for_worker() { + for _attempt in $(seq 1 90); do + if docker exec "$WORKER_CONTAINER" \ + python -c 'from shared.services.worker_health import assert_worker_healthy; assert_worker_healthy()' >/dev/null 2>&1; then + return 0 + fi + if ! docker inspect --format '{{.State.Running}}' "$WORKER_CONTAINER" 2>/dev/null | grep --quiet true; then + docker logs "$WORKER_CONTAINER" || true + return 1 + fi + sleep 2 + done + docker logs "$WORKER_CONTAINER" || true + return 1 +} + +cleanup +docker network create "$NETWORK_NAME" >/dev/null + +docker run --detach --name "$POSTGRES_CONTAINER" --network "$NETWORK_NAME" \ + --env POSTGRES_DB=Knowhere \ + --env POSTGRES_USER=root \ + --env POSTGRES_PASSWORD=root123 \ + "$POSTGRES_IMAGE" >/dev/null +docker run --detach --name "$REDIS_CONTAINER" --network "$NETWORK_NAME" \ + "$REDIS_IMAGE" >/dev/null + +wait_for_postgres +wait_for_redis + +build_arguments=() +if [[ -n "${http_proxy:-}" ]]; then + build_arguments+=(--build-arg "http_proxy=${http_proxy}") +fi +if [[ -n "${https_proxy:-}" ]]; then + build_arguments+=(--build-arg "https_proxy=${https_proxy}") +fi + +docker build "${build_arguments[@]}" --file deploy/docker/Dockerfile.api --tag "$API_IMAGE" . +docker build "${build_arguments[@]}" --file deploy/docker/Dockerfile.worker --tag "$WORKER_IMAGE" . + +common_environment=( + --env ENVIRONMENT=development + --env APP_ENV=development + --env TMP_PATH=/tmp/knowhere + --env DATABASE_URL=postgresql+asyncpg://root:root123@${POSTGRES_CONTAINER}:5432/Knowhere + --env DB_SSL_MODE=disable + --env DB_POOL_SIZE=2 + --env DB_MAX_OVERFLOW=2 + --env DB_SYNC_POOL_SIZE=2 + --env DB_SYNC_MAX_OVERFLOW=2 + --env REDIS_HOST="$REDIS_CONTAINER" + --env REDIS_PORT=6379 + --env REDIS_DATABASE=0 + --env REDIS_SSL=false + --env CELERY_REDIS_URL=redis://${REDIS_CONTAINER}:6379/0 + --env S3_TYPE=filesystem + --env S3_BUCKET_NAME=knowhere-runtime-smoke + --env S3_RESULTS_BUCKET=knowhere-runtime-smoke + --env S3_TEMP_PATH=/tmp + --env OBJECT_STORAGE_LOCAL_ROOT=/tmp/object-storage + --env TELEMETRY_ENABLED=false + --env LOGFIRE_TOKEN= + --env LLM_MOCK_ENABLED=true + --env BILLING_ENABLED=false + --env API_STANDALONE_MODE_ENABLED=true + --env RATE_LIMIT_ENABLED=false +) + +docker run --rm --network "$NETWORK_NAME" \ + "${common_environment[@]}" \ + --entrypoint python "$API_IMAGE" -m alembic upgrade heads + +docker run --detach --name "$API_CONTAINER" --network "$NETWORK_NAME" \ + "${common_environment[@]}" \ + --env INTERNAL_DASHBOARD_ENDPOINT=http://localhost:3000 \ + "$API_IMAGE" >/dev/null + +docker run --detach --name "$WORKER_CONTAINER" --network "$NETWORK_NAME" \ + "${common_environment[@]}" \ + --env WORKER_CONCURRENCY=1 \ + "$WORKER_IMAGE" >/dev/null + +wait_for_api +wait_for_worker + +printf 'Docker runtime smoke test passed.\n' +printf ' API image: %s\n' "$API_IMAGE" +printf ' Worker image: %s\n' "$WORKER_IMAGE"