Files
tg-vault/deploy/install-runtime.sh

158 lines
6.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
# Runtime checks are intentionally independent of .env creation.
installer_compose() {
env SOURCE_REVISION="$RELEASE_REVISION" SOURCE_VERSION="$RELEASE_VERSION" docker compose ${INSTALL_COMPOSE_ARGS[@]+"${INSTALL_COMPOSE_ARGS[@]}"} "$@"
}
prepare_install_compose() {
INSTALL_COMPOSE_ARGS=()
INSTALL_COMPAT_FILE=''
INSTALL_SOURCE_IMAGE_FILE=''
INSTALL_QBITTORRENT_INTEGRATION="$(read_env QBITTORRENT_HOST_INTEGRATION)"
if [[ "$INSTALL_QBITTORRENT_INTEGRATION" == true ]]; then
INSTALL_COMPOSE_ARGS=(--project-directory "$PWD" --env-file "$PWD/.env" -f "$PWD/docker-compose.yml" -f "$PWD/docker-compose.qbittorrent.yml")
fi
trap 'rm -f "${INSTALL_COMPAT_FILE:-}" "${INSTALL_SOURCE_IMAGE_FILE:-}"' EXIT
}
prepare_source_build() {
local override
if [[ -n "${COMPOSE_FILE:-}" ]]; then
echo "源码兜底不支持 COMPOSE_FILE;请使用项目的标准 Compose 配置。" >&2
return 1
fi
for override in compose.override.yaml compose.override.yml docker-compose.override.yml docker-compose.override.yaml; do
if [[ -f "$override" ]]; then
echo "源码兜底检测到额外 Compose 覆盖文件;为保护自定义配置,已停止自动切换。" >&2
return 1
fi
done
INSTALL_COMPOSE_ARGS=(--project-directory "$PWD" --env-file "$PWD/.env" -f "$PWD/docker-compose.yml")
if [[ "${INSTALL_REQUIRE_ATTESTATIONS:-true}" == false ]]; then
INSTALL_COMPAT_FILE="$(mktemp "${TMPDIR:-/tmp}/tg-vault-build-compat.XXXXXX.yml")"
python3 - "$PWD/docker-compose.build.yml" "$INSTALL_COMPAT_FILE" <<'PY'
from pathlib import Path
import re, sys
source, target = map(Path, sys.argv[1:])
target.write_text(''.join(line for line in source.read_text().splitlines(keepends=True)
if not re.match(r'^ (sbom|provenance):', line)))
PY
INSTALL_COMPOSE_ARGS+=(-f "$INSTALL_COMPAT_FILE")
else
INSTALL_COMPOSE_ARGS+=(-f "$PWD/docker-compose.build.yml")
fi
if [[ "$INSTALL_QBITTORRENT_INTEGRATION" == true ]]; then
INSTALL_COMPOSE_ARGS+=(-f "$PWD/docker-compose.qbittorrent.yml")
fi
INSTALL_SOURCE_IMAGE_FILE="$(mktemp "${TMPDIR:-/tmp}/tg-vault-source-images.XXXXXX.yml")"
python3 - "$INSTALL_SOURCE_IMAGE_FILE" "$$" <<'PY'
import json, sys
from pathlib import Path
Path(sys.argv[1]).write_text(json.dumps({'services': {
service: {'image': f'tg-vault-{"backend" if service == "db-migrate" else service}:source-{sys.argv[2]}'}
for service in ('backend', 'frontend', 'db-migrate')}}))
PY
INSTALL_COMPOSE_ARGS+=(-f "$INSTALL_SOURCE_IMAGE_FILE")
}
verify_application_image_pair() {
local service image expected='' revision
for service in backend frontend; do
image="$(installer_compose config --format json | python3 -c 'import json,sys; print(json.load(sys.stdin)["services"][sys.argv[1]]["image"])' "$service")"
revision="$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$image")" || return 1
if [[ ! "$revision" =~ ^[0-9a-f]{40}$ ]]; then
echo "镜像缺少有效源码修订,不能确认前后端属于同一发布。" >&2
return 1
fi
if [[ -n "$expected" && "$revision" != "$expected" ]]; then
echo "前后端 latest 镜像修订不一致,暂不替换线上服务。" >&2
return 1
fi
expected="$revision"
done
}
build_application_images() {
echo '切换到本地源码构建;数据库、文件挂载和已有密钥保持不变。'
INSTALL_STAGE=source-environment
INSTALL_CHECK_BUILD=true check_environment
prepare_source_build
installer_compose config --quiet
installer_compose config --format json | python3 "$SCRIPT_DIR/install-storage.py" verify
INSTALL_STAGE=build-backend
installer_compose build backend
INSTALL_STAGE=build-frontend
installer_compose build frontend
}
assert_database_credentials_safe() {
# Inspect only. Existing data must never receive a newly generated password.
local password="$1" project volume containers
[[ -n "$password" ]] && return 0
project="${COMPOSE_PROJECT_NAME:-tg-vault}"
volume="${project}_postgres-data"
containers="$(docker ps -aq --filter "label=com.docker.compose.project=$project" --filter 'label=com.docker.compose.service=postgres')"
if [[ -n "$containers" ]] || docker volume inspect "$volume" >/dev/null 2>&1; then
echo "检测到已有 PostgreSQL 容器或数据卷,但 .env 缺少 DB_PASSWORD。" >&2
echo "已停止,避免新密码导致数据库无法登录。请恢复原 .env/数据库密码后重试,不要删除数据卷。" >&2
return 1
fi
}
installer_failure() {
local status="$1"
trap - ERR
echo "TG Vault 部署失败(阶段:${INSTALL_STAGE:-unknown},退出码:$status)。" >&2
echo "未自动删除数据卷或重置密码。排障请运行:" >&2
echo " docker compose ps --all" >&2
echo " docker compose logs --tail=80 postgres backend frontend" >&2
installer_compose ps --all >&2 || true
exit "$status"
}
validate_install_timeout() {
local timeout="${INSTALL_HEALTH_TIMEOUT:-180}"
if [[ ! "$timeout" =~ ^[1-9][0-9]{1,3}$ ]] || (( timeout < 30 || timeout > 1800 )); then
echo "INSTALL_HEALTH_TIMEOUT 必须为 30–1800 秒的整数。" >&2
return 1
fi
}
start_installation() {
local timeout="${INSTALL_HEALTH_TIMEOUT:-180}"
validate_install_timeout
trap 'installer_failure $?' ERR
INSTALL_STAGE=config
installer_compose config --quiet
installer_compose config --format json | python3 "$SCRIPT_DIR/install-storage.py" verify
if [[ "${INSTALL_FROM_SOURCE:-false}" == true ]]; then
build_application_images
else
INSTALL_STAGE=pull-application
echo '正在拉取 GHCR 最新前后端镜像(latest)...'
if installer_compose pull backend frontend && verify_application_image_pair; then
echo '最新镜像拉取完成;无需在本机编译。'
else
echo '镜像拉取失败、架构不支持或发布不完整,使用本地源码兜底。' >&2
build_application_images
fi
fi
INSTALL_STAGE=postgres
# The first install MUST start its dependency. Existing database containers are
# never recreated for an application upgrade; stopped ones may be started.
installer_compose up -d --no-build --no-deps --no-recreate --wait --wait-timeout "$timeout" postgres
INSTALL_STAGE=stop-application
# Quiesce old workers before DDL. On failure leave the old app stopped: its
# image may no longer be compatible with a partially upgraded database.
installer_compose stop backend frontend
INSTALL_STAGE=database-plan
installer_compose run --rm --no-deps --pull never backend node dist/scripts/database.js plan
INSTALL_STAGE=database-migrate
installer_compose run --rm --no-deps --pull never backend node dist/scripts/database.js migrate
INSTALL_STAGE=application
installer_compose up -d --no-build --pull never --no-deps --wait --wait-timeout "$timeout" backend frontend
INSTALL_STAGE=verify
installer_compose ps
trap - ERR
}