284 lines
8.1 KiB
Bash
Executable File
284 lines
8.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
|
|
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-easyai-ai-gateway}"
|
|
AI_GATEWAY_PLATFORM="${AI_GATEWAY_PLATFORM:-linux/amd64}"
|
|
AI_GATEWAY_IMAGE_REGISTRY="${AI_GATEWAY_IMAGE_REGISTRY:-registry.cn-shanghai.aliyuncs.com/easyaigc}"
|
|
AI_GATEWAY_IMAGE_TAG="${AI_GATEWAY_IMAGE_TAG:-latest}"
|
|
AI_GATEWAY_API_IMAGE="${AI_GATEWAY_API_IMAGE:-${AI_GATEWAY_IMAGE_REGISTRY}/ai-gateway:${AI_GATEWAY_IMAGE_TAG}}"
|
|
AI_GATEWAY_WEB_IMAGE="${AI_GATEWAY_WEB_IMAGE:-${AI_GATEWAY_IMAGE_REGISTRY}/ai-gateway-web:${AI_GATEWAY_IMAGE_TAG}}"
|
|
ACTION="${1:-deploy}"
|
|
|
|
export COMPOSE_PROJECT_NAME
|
|
export AI_GATEWAY_PLATFORM
|
|
export AI_GATEWAY_API_IMAGE
|
|
export AI_GATEWAY_WEB_IMAGE
|
|
export DOCKER_DEFAULT_PLATFORM="${DOCKER_DEFAULT_PLATFORM:-$AI_GATEWAY_PLATFORM}"
|
|
|
|
compose=(docker compose -f "$COMPOSE_FILE")
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
scripts/deploy-compose.sh Build, migrate, start, and verify
|
|
scripts/deploy-compose.sh deploy Same as default
|
|
scripts/deploy-compose.sh push Build and push API/Web images
|
|
scripts/deploy-compose.sh down Stop containers, keep volumes
|
|
scripts/deploy-compose.sh clean Stop containers and remove volumes
|
|
|
|
Useful environment overrides:
|
|
AI_GATEWAY_PLATFORM=linux/amd64
|
|
AI_GATEWAY_IMAGE_TAG=2026.05.23-1
|
|
AI_GATEWAY_API_IMAGE=registry.cn-shanghai.aliyuncs.com/easyaigc/ai-gateway:2026.05.23-1
|
|
AI_GATEWAY_WEB_IMAGE=registry.cn-shanghai.aliyuncs.com/easyaigc/ai-gateway-web:2026.05.23-1
|
|
AI_GATEWAY_WEB_PORT=5178
|
|
AI_GATEWAY_API_PORT=8088
|
|
AI_GATEWAY_DB_PORT=54329
|
|
AI_GATEWAY_PUSH=1
|
|
AI_GATEWAY_SKIP_BUILD=1
|
|
EOF
|
|
}
|
|
|
|
fail_with_logs() {
|
|
echo "[ai-gateway] deployment failed; recent container state follows" >&2
|
|
"${compose[@]}" ps >&2 || true
|
|
"${compose[@]}" logs --tail=160 postgres migrator api web >&2 || true
|
|
exit 1
|
|
}
|
|
|
|
remove_stale_migrator() {
|
|
"${compose[@]}" rm -sf migrator >/dev/null 2>&1 || true
|
|
}
|
|
|
|
wait_for_service_healthy() {
|
|
local service="$1"
|
|
local container_id=""
|
|
local status=""
|
|
|
|
for _ in $(seq 1 60); do
|
|
container_id="$("${compose[@]}" ps -q "$service" 2>/dev/null || true)"
|
|
if [[ -n "$container_id" ]]; then
|
|
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id" 2>/dev/null || true)"
|
|
if [[ "$status" == "healthy" || "$status" == "running" ]]; then
|
|
echo "[ai-gateway] ${service} is ${status}"
|
|
return 0
|
|
fi
|
|
if [[ "$status" == "exited" || "$status" == "dead" ]]; then
|
|
echo "[ai-gateway] ${service} stopped while waiting for health" >&2
|
|
fail_with_logs
|
|
fi
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
echo "[ai-gateway] timed out waiting for ${service} to become healthy" >&2
|
|
fail_with_logs
|
|
}
|
|
|
|
wait_for_http() {
|
|
local label="$1"
|
|
local url="$2"
|
|
local expected="${3:-}"
|
|
local body=""
|
|
|
|
for _ in $(seq 1 60); do
|
|
if body="$(curl -fsS --max-time 5 "$url" 2>/dev/null)"; then
|
|
if [[ -z "$expected" || "$body" == *"$expected"* ]]; then
|
|
echo "[ai-gateway] verified ${label}: ${url}"
|
|
return 0
|
|
fi
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
echo "[ai-gateway] timed out waiting for ${label}: ${url}" >&2
|
|
[[ -n "$body" ]] && echo "$body" >&2
|
|
fail_with_logs
|
|
}
|
|
|
|
published_port() {
|
|
local service="$1"
|
|
local private_port="$2"
|
|
local fallback="$3"
|
|
local endpoint=""
|
|
|
|
endpoint="$("${compose[@]}" port "$service" "$private_port" 2>/dev/null | tail -n 1 || true)"
|
|
if [[ -n "$endpoint" ]]; then
|
|
echo "${endpoint##*:}"
|
|
else
|
|
echo "$fallback"
|
|
fi
|
|
}
|
|
|
|
build_images() {
|
|
if [[ "${AI_GATEWAY_SKIP_BUILD:-0}" != "1" ]]; then
|
|
echo "[ai-gateway] building images"
|
|
echo "[ai-gateway] api image: ${AI_GATEWAY_API_IMAGE}"
|
|
echo "[ai-gateway] web image: ${AI_GATEWAY_WEB_IMAGE}"
|
|
"${compose[@]}" build --pull
|
|
else
|
|
echo "[ai-gateway] skipping image build"
|
|
fi
|
|
}
|
|
|
|
package_version() {
|
|
local version=""
|
|
|
|
version="$(
|
|
sed -nE 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$PROJECT_ROOT/package.json" \
|
|
| head -n 1
|
|
)"
|
|
if [[ -z "$version" ]]; then
|
|
echo "[ai-gateway] cannot infer package version from package.json; set AI_GATEWAY_IMAGE_TAG" >&2
|
|
exit 1
|
|
fi
|
|
echo "$version"
|
|
}
|
|
|
|
push_version_tag() {
|
|
if [[ "$AI_GATEWAY_IMAGE_TAG" == "latest" ]]; then
|
|
package_version
|
|
else
|
|
echo "$AI_GATEWAY_IMAGE_TAG"
|
|
fi
|
|
}
|
|
|
|
image_repository() {
|
|
local image="$1"
|
|
echo "${image%:*}"
|
|
}
|
|
|
|
tag_release_images() {
|
|
local version_tag="$1"
|
|
local api_repo web_repo
|
|
local api_version_image api_latest_image web_version_image web_latest_image
|
|
|
|
api_repo="$(image_repository "$AI_GATEWAY_API_IMAGE")"
|
|
web_repo="$(image_repository "$AI_GATEWAY_WEB_IMAGE")"
|
|
api_version_image="${api_repo}:${version_tag}"
|
|
api_latest_image="${api_repo}:latest"
|
|
web_version_image="${web_repo}:${version_tag}"
|
|
web_latest_image="${web_repo}:latest"
|
|
|
|
docker image inspect "$AI_GATEWAY_API_IMAGE" >/dev/null 2>&1 || {
|
|
echo "[ai-gateway] missing local API image: ${AI_GATEWAY_API_IMAGE}; build it first" >&2
|
|
exit 1
|
|
}
|
|
docker image inspect "$AI_GATEWAY_WEB_IMAGE" >/dev/null 2>&1 || {
|
|
echo "[ai-gateway] missing local Web image: ${AI_GATEWAY_WEB_IMAGE}; build it first" >&2
|
|
exit 1
|
|
}
|
|
|
|
docker tag "$AI_GATEWAY_API_IMAGE" "$api_version_image"
|
|
docker tag "$AI_GATEWAY_API_IMAGE" "$api_latest_image"
|
|
docker tag "$AI_GATEWAY_WEB_IMAGE" "$web_version_image"
|
|
docker tag "$AI_GATEWAY_WEB_IMAGE" "$web_latest_image"
|
|
|
|
RELEASE_IMAGES=(
|
|
"$api_version_image"
|
|
"$api_latest_image"
|
|
"$web_version_image"
|
|
"$web_latest_image"
|
|
)
|
|
}
|
|
|
|
push_images() {
|
|
local version_tag
|
|
local image
|
|
|
|
version_tag="$(push_version_tag)"
|
|
echo "[ai-gateway] release image tag: ${version_tag}"
|
|
echo "[ai-gateway] latest tag will also be pushed"
|
|
tag_release_images "$version_tag"
|
|
|
|
for image in "${RELEASE_IMAGES[@]}"; do
|
|
echo "[ai-gateway] pushing image: ${image}"
|
|
if ! docker push "$image"; then
|
|
echo "[ai-gateway] failed to push image; login may be required:" >&2
|
|
echo "[ai-gateway] docker login --username=<your-aliyun-account> registry.cn-shanghai.aliyuncs.com" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
deploy() {
|
|
cd "$PROJECT_ROOT"
|
|
|
|
command -v docker >/dev/null 2>&1 || {
|
|
echo "[ai-gateway] docker is required" >&2
|
|
exit 1
|
|
}
|
|
docker compose version >/dev/null 2>&1 || {
|
|
echo "[ai-gateway] docker compose v2 is required" >&2
|
|
exit 1
|
|
}
|
|
command -v curl >/dev/null 2>&1 || {
|
|
echo "[ai-gateway] curl is required for deployment verification" >&2
|
|
exit 1
|
|
}
|
|
|
|
echo "[ai-gateway] compose project: ${COMPOSE_PROJECT_NAME}"
|
|
echo "[ai-gateway] target platform: ${AI_GATEWAY_PLATFORM}"
|
|
echo "[ai-gateway] image tag: ${AI_GATEWAY_IMAGE_TAG}"
|
|
|
|
build_images
|
|
|
|
if [[ "${AI_GATEWAY_PUSH:-0}" == "1" ]]; then
|
|
push_images
|
|
fi
|
|
|
|
echo "[ai-gateway] starting postgres"
|
|
"${compose[@]}" up -d postgres
|
|
wait_for_service_healthy postgres
|
|
|
|
echo "[ai-gateway] running database migrations"
|
|
remove_stale_migrator
|
|
"${compose[@]}" run --rm migrator
|
|
remove_stale_migrator
|
|
|
|
echo "[ai-gateway] starting api and web"
|
|
"${compose[@]}" up -d api web
|
|
|
|
local api_port
|
|
local web_port
|
|
api_port="$(published_port api 8088 "${AI_GATEWAY_API_PORT:-8088}")"
|
|
web_port="$(published_port web 80 "${AI_GATEWAY_WEB_PORT:-5178}")"
|
|
|
|
wait_for_http "api health" "http://127.0.0.1:${api_port}/healthz" "easyai-ai-gateway"
|
|
wait_for_http "api readiness" "http://127.0.0.1:${api_port}/readyz" '"ok":true'
|
|
wait_for_http "web reverse proxy" "http://127.0.0.1:${web_port}/gateway-api/healthz" "easyai-ai-gateway"
|
|
wait_for_http "web app" "http://127.0.0.1:${web_port}/" "EasyAI AI Gateway"
|
|
|
|
echo "[ai-gateway] deployment succeeded"
|
|
echo "[ai-gateway] Web: http://127.0.0.1:${web_port}"
|
|
echo "[ai-gateway] API: http://127.0.0.1:${api_port}/healthz"
|
|
}
|
|
|
|
case "$ACTION" in
|
|
deploy|up)
|
|
deploy
|
|
;;
|
|
push)
|
|
cd "$PROJECT_ROOT"
|
|
build_images
|
|
push_images
|
|
;;
|
|
down)
|
|
cd "$PROJECT_ROOT"
|
|
"${compose[@]}" down --remove-orphans
|
|
;;
|
|
clean)
|
|
cd "$PROJECT_ROOT"
|
|
"${compose[@]}" down -v --remove-orphans
|
|
;;
|
|
-h|--help|help)
|
|
usage
|
|
;;
|
|
*)
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|