This commit is contained in:
2026-09-29 18:47:37 +08:00
parent 2c60a29e0f
commit c2927ab9a6
291 changed files with 4008 additions and 6298 deletions
-411
View File
@@ -1,411 +0,0 @@
#!/usr/bin/env sh
set -eu
if [ "$#" -ne 7 ]; then
echo "用法: $0 <环境> <镜像仓库> <镜像标签> <服务> <环境文件> <Compose文件> <项目名>" >&2
exit 2
fi
DEPLOY_ENV="$1"
IMAGE_REPOSITORY="$2"
IMAGE_TAG="$3"
SERVICE="$4"
ENV_FILE="$5"
COMPOSE_FILE="$6"
BASE_PROJECT="$7"
case "$DEPLOY_ENV" in
test|production) ;;
*) echo "蓝绿执行器仅用于 test/production,当前: $DEPLOY_ENV" >&2; exit 2 ;;
esac
for file in "$ENV_FILE" "$COMPOSE_FILE"; do
[ -f "$file" ] || { echo "必需文件不存在: $file" >&2; exit 2; }
done
case "$SERVICE" in
*[!A-Za-z0-9_-]*|'') echo "非法服务名: $SERVICE" >&2; exit 2 ;;
esac
env_value() {
sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1 | tr -d '\r'
}
service_contract() {
case "$SERVICE:$COMPOSE_FILE" in
gateway:*) echo "gateway|9000|9000|gateway|gateway-active" ;;
auth:*) echo "auth|9100|9100|auth-service|auth" ;;
iam:*) echo "iam|9201|9201|iam-service|iam" ;;
firstleg:*) echo "business|9202|9202|firstleg-service|firstleg" ;;
finance:*) echo "finance|9203|9203|finance-service|finance" ;;
order:*) echo "order|9204|9204|order-service|order" ;;
warehouse:*) echo "warehouse|9205|9205|warehouse-service|warehouse" ;;
file:*) echo "file|9206|9206|file-service|file" ;;
print:*) echo "print|9207|9207|print-service|print" ;;
integration-service:*) echo "integration|9210|9210|integration-service|integration-service" ;;
audit-service:*) echo "audit|9700|9700|audit-service|audit-service" ;;
gateway-overseas:*) echo "gateway-overseas|9300|9300|gateway-overseas|gateway-overseas-active" ;;
lastleg-service:*) echo "lastleg|9303|9303|lastleg-service|lastleg-service" ;;
warehouse-overseas-service:*) echo "warehouse-overseas|9305|9305|warehouse-overseas-service|warehouse-overseas-service" ;;
event-bridge-service:*overseas*) echo "event-bridge-overseas|9312|9313|event-bridge-overseas|event-bridge-overseas" ;;
event-bridge-service:*) echo "event-bridge|9312|9312|event-bridge-cn|event-bridge-cn" ;;
*) echo "不支持蓝绿部署的服务: $SERVICE" >&2; exit 2 ;;
esac
}
IFS='|' read -r CONTAINER_STEM INTERNAL_PORT BASE_PORT NACOS_SERVICE ROUTING_ALIAS <<EOF
$(service_contract)
EOF
STATE_ROOT="${BLUE_GREEN_STATE_ROOT:-/data/firstleg/deploy/blue-green-state}"
STATE_KEY="${BASE_PROJECT}-${SERVICE}"
STATE_FILE="$STATE_ROOT/$STATE_KEY.active"
ROUTING_NETWORK="${BLUE_GREEN_ROUTING_NETWORK:-firstleg-${DEPLOY_ENV}-routing}"
HEALTH_TIMEOUT="${DEPLOY_HEALTH_TIMEOUT_SECONDS:-240}"
HEALTH_POLL="${DEPLOY_HEALTH_POLL_SECONDS:-5}"
STABILIZATION_SECONDS="${BLUE_GREEN_STABILIZATION_SECONDS:-10}"
DRAIN_SECONDS="${BLUE_GREEN_DRAIN_SECONDS:-30}"
STOP_TIMEOUT="${BLUE_GREEN_STOP_TIMEOUT_SECONDS:-45}"
IMAGE="$IMAGE_REPOSITORY/$SERVICE:$IMAGE_TAG"
for value in "$HEALTH_TIMEOUT" "$HEALTH_POLL" "$STABILIZATION_SECONDS" "$DRAIN_SECONDS" "$STOP_TIMEOUT"; do
case "$value" in ''|*[!0-9]*) echo "蓝绿时间参数必须为非负整数: $value" >&2; exit 2 ;; esac
done
mkdir -p "$STATE_ROOT"
chmod 750 "$STATE_ROOT"
docker network inspect "$ROUTING_NETWORK" >/dev/null 2>&1 || docker network create "$ROUTING_NETWORK" >/dev/null
legacy_container="firstleg-${DEPLOY_ENV}-${CONTAINER_STEM}"
active_slot=""
active_container=""
active_port=""
active_worker=""
if [ -s "$STATE_FILE" ]; then
IFS='|' read -r active_slot active_container active_port active_image active_worker < "$STATE_FILE"
elif docker inspect "$legacy_container" >/dev/null 2>&1; then
active_slot="legacy"
active_container="$legacy_container"
active_port="$BASE_PORT"
fi
if [ "$active_slot" = "blue" ]; then
target_slot="green"
else
target_slot="blue"
[ "$active_slot" = "legacy" ] && target_slot="green"
fi
if [ "$target_slot" = "blue" ]; then
target_port=$((BASE_PORT + 20000))
else
target_port=$((BASE_PORT + 10000))
fi
candidate="firstleg-${DEPLOY_ENV}-${CONTAINER_STEM}-${target_slot}"
worker_candidate="${candidate}-worker"
if docker inspect "$candidate" >/dev/null 2>&1; then
if [ "$candidate" = "$active_container" ]; then
echo "活动槽状态异常,候选容器与活动容器相同: $candidate" >&2
exit 1
fi
docker rm -f "$candidate" >/dev/null
fi
compose() {
docker compose --project-name "$BASE_PROJECT" --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
}
container_health() {
container="$1"
docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \
"$container" 2>/dev/null || echo missing
}
cleanup_candidate() {
docker rm -f "$candidate" >/dev/null 2>&1 || true
}
wait_for_container() {
container="$1"
label="$2"
elapsed=0
while [ "$elapsed" -lt "$HEALTH_TIMEOUT" ]; do
state="$(container_health "$container")"
echo "$label 健康状态: $state(已等待 ${elapsed}s)"
case "$state" in
healthy) return 0 ;;
unhealthy|exited|dead|missing) return 1 ;;
esac
sleep "$HEALTH_POLL"
elapsed=$((elapsed + HEALTH_POLL))
done
return 1
}
needs_worker() {
case "$SERVICE" in
iam|firstleg|finance|order|event-bridge-service|lastleg-service) return 0 ;;
file)
[ "$(env_value FILE_EXPORT_CONSUMER_ENABLED)" = true ] \
|| [ "$(env_value FILE_EXPORT_PUBLISHER_ENABLED)" = true ] \
|| [ "$(env_value FILE_EXPORT_RECONCILER_ENABLED)" = true ] \
|| [ "$(env_value AUDIT_OUTBOX_PUBLISHER_ENABLED)" = true ]
;;
audit-service)
[ "$(env_value AUDIT_MQ_CONSUMER_ENABLED)" = true ] \
|| [ "$(env_value AUDIT_DLQ_CONSUMER_ENABLED)" = true ] \
|| [ "$(env_value AUDIT_DLQ_COMPENSATION_ENABLED)" = true ]
;;
auth|warehouse|warehouse-overseas-service)
[ "$(env_value AUDIT_OUTBOX_PUBLISHER_ENABLED)" = true ]
;;
*) return 1 ;;
esac
}
start_worker_candidate() {
docker rm -f "$worker_candidate" >/dev/null 2>&1 || true
echo "启动单例 Worker: $worker_candidate"
if [ "$SERVICE" = file ]; then
worker_java_options="-Xms512m -Xmx2g -XX:+ExitOnOutOfMemoryError -Dfile.encoding=UTF-8"
else
worker_java_options="-Xms256m -Xmx1g -XX:+ExitOnOutOfMemoryError -Dfile.encoding=UTF-8"
fi
if ! compose run -d --no-deps --name "$worker_candidate" \
-e "NACOS_REGISTER_ENABLED=false" \
-e "BAIYUE_RUNTIME_SCHEDULING_ENABLED=true" \
-e "BAIYUE_DEPLOY_SLOT=${target_slot}-worker" \
-e "JAVA_TOOL_OPTIONS=$worker_java_options" \
-e "BAIYUE_RELEASE_ID=$IMAGE_TAG" "$SERVICE" >/dev/null; then
return 1
fi
docker network connect "$ROUTING_NETWORK" "$worker_candidate"
if ! wait_for_container "$worker_candidate" "候选 Worker"; then
docker logs --tail 200 "$worker_candidate" 2>&1 || true
docker rm -f "$worker_candidate" >/dev/null 2>&1 || true
return 1
fi
worker_image="$(docker inspect --format '{{.Config.Image}}' "$worker_candidate")"
[ "$worker_image" = "$IMAGE" ] || return 1
}
connect_routing_aliases() {
container="$1"
shift
docker network inspect "$ROUTING_NETWORK" --format '{{json .Containers}}' | grep -q "\"$container\"" \
&& return 0
set -- docker network connect $(for alias_name in "$@"; do printf '%s\n' --alias "$alias_name"; done)
"$@" "$ROUTING_NETWORK" "$container"
}
bootstrap_legacy_routes() {
for pair in \
gateway-active,gateway:gateway auth,auth-service:auth iam,iam-service:iam \
firstleg,firstleg-service:business finance,finance-service:finance order,order-service:order \
warehouse,warehouse-service:warehouse file,file-service:file print,print-service:print \
integration-service:integration audit-service:audit \
gateway-overseas-active,gateway-overseas:gateway-overseas \
lastleg-service:lastleg warehouse-overseas-service:warehouse-overseas \
event-bridge-cn:event-bridge event-bridge-overseas:event-bridge-overseas
do
aliases="${pair%%:*}"
stem="${pair#*:}"
container="firstleg-${DEPLOY_ENV}-${stem}"
if docker inspect "$container" >/dev/null 2>&1; then
connect_routing_aliases "$container" $(printf '%s' "$aliases" | tr ',' ' ') || true
fi
done
for container in firstleg-prod-admin-web firstleg-prod-client-web; do
if docker inspect "$container" >/dev/null 2>&1; then
connect_routing_aliases "$container" "${container##*-}" || true
fi
done
}
switch_gateway_proxy() {
[ "$SERVICE" = "gateway" ] || [ "$SERVICE" = "gateway-overseas" ] || return 0
if [ "$DEPLOY_ENV" = test ]; then
echo "测试 Gateway 仍由固定入口承载,不在后端蓝绿执行器中切换" >&2
return 1
fi
if [ "$SERVICE" = "gateway" ]; then required_alias=gateway-active; else required_alias=gateway-overseas-active; fi
for frontend_service in admin-web client-web; do
frontend_state="$STATE_ROOT/frontend-$frontend_service.active"
if [ ! -s "$frontend_state" ]; then
echo "$frontend_service 尚未完成首次蓝绿迁移,拒绝切换 Gateway" >&2
return 1
fi
IFS='|' read -r frontend_slot frontend frontend_port frontend_image < "$frontend_state"
if ! docker inspect "$frontend" >/dev/null 2>&1 \
|| ! docker exec "$frontend" nginx -T 2>&1 | grep -q "$required_alias"; then
echo "$frontend 尚未使用稳定上游 $required_alias,拒绝切换 Gateway" >&2
return 1
fi
done
route_root="${BLUE_GREEN_ROUTE_CONFIG_ROOT:-/www/server/panel/vhost/nginx}"
if [ "$SERVICE" = "gateway" ]; then
config="$route_root/00-blue-green-gateway.conf"
else
config="$route_root/00-blue-green-gateway-overseas.conf"
fi
if [ -z "$config" ] || [ ! -f "$config" ]; then
echo "$SERVICE 蓝绿切换要求环境文件配置有效的 Nginx 配置路径" >&2
return 1
fi
backup="$config.blue-green-backup"
cp -f "$config" "$backup"
if ! sed -i "s#127\.0\.0\.1:${active_port:-$BASE_PORT}#127.0.0.1:$target_port#g" "$config" \
|| cmp -s "$config" "$backup"; then
cp -f "$backup" "$config"
echo "未能在 Nginx 配置中切换 $SERVICE 上游端口" >&2
return 1
fi
if ! nginx -t || ! nginx -s reload; then
cp -f "$backup" "$config"
nginx -t && nginx -s reload || true
return 1
fi
rm -f "$backup"
}
reload_frontend_proxies() {
[ "$SERVICE" = "gateway" ] || [ "$SERVICE" = "gateway-overseas" ] || return 0
[ "$DEPLOY_ENV" = production ] || return 0
for frontend_service in admin-web client-web; do
frontend_state="$STATE_ROOT/frontend-$frontend_service.active"
[ -s "$frontend_state" ] || return 1
IFS='|' read -r frontend_slot frontend frontend_port frontend_image < "$frontend_state"
docker inspect "$frontend" >/dev/null 2>&1 || return 1
docker exec "$frontend" nginx -t
docker exec "$frontend" nginx -s reload
done
}
rollback_gateway_proxy() {
[ "$SERVICE" = "gateway" ] || [ "$SERVICE" = "gateway-overseas" ] || return 0
[ "$DEPLOY_ENV" = production ] || return 0
route_root="${BLUE_GREEN_ROUTE_CONFIG_ROOT:-/www/server/panel/vhost/nginx}"
if [ "$SERVICE" = "gateway" ]; then
config="$route_root/00-blue-green-gateway.conf"
else
config="$route_root/00-blue-green-gateway-overseas.conf"
fi
old_port="${active_port:-$BASE_PORT}"
sed -i "s#127\.0\.0\.1:$target_port#127.0.0.1:$old_port#g" "$config"
nginx -t && nginx -s reload
}
bootstrap_legacy_routes
export DEPLOY_ENV IMAGE_REPOSITORY IMAGE_TAG
echo "启动候选槽: service=$SERVICE slot=$target_slot port=$target_port image=$IMAGE"
if ! compose run -d --no-deps --name "$candidate" \
-e "BAIYUE_DEPLOY_SLOT=$target_slot" -e "BAIYUE_RELEASE_ID=$IMAGE_TAG" \
-e "BAIYUE_RUNTIME_SCHEDULING_ENABLED=false" \
-e "AUDIT_OUTBOX_PUBLISHER_ENABLED=false" \
-e "NOTIFICATION_SCHEDULER_ENABLED=false" \
-e "NOTIFICATION_MQ_CONSUMER_ENABLED=false" \
-e "FILE_EXPORT_CONSUMER_ENABLED=false" \
-e "FILE_EXPORT_PUBLISHER_ENABLED=false" \
-e "FILE_EXPORT_RECONCILER_ENABLED=false" \
-e "AUDIT_MQ_CONSUMER_ENABLED=false" \
-e "AUDIT_DLQ_CONSUMER_ENABLED=false" \
-e "AUDIT_DLQ_COMPENSATION_ENABLED=false" \
-p "${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:$target_port:$INTERNAL_PORT" "$SERVICE" >/dev/null; then
cleanup_candidate
exit 1
fi
if ! wait_for_container "$candidate" "候选 Web 槽 $target_slot"; then
echo "候选槽未通过健康检查,保留旧实例" >&2
docker logs --tail 200 "$candidate" 2>&1 || true
cleanup_candidate
exit 1
fi
running_image="$(docker inspect --format '{{.Config.Image}}' "$candidate")"
if [ "$running_image" != "$IMAGE" ]; then
echo "候选镜像不一致: expected=$IMAGE actual=$running_image" >&2
cleanup_candidate
exit 1
fi
if [ "$NACOS_SERVICE" = "$ROUTING_ALIAS" ]; then
connect_routing_aliases "$candidate" "$ROUTING_ALIAS"
else
connect_routing_aliases "$candidate" "$ROUTING_ALIAS" "$NACOS_SERVICE"
fi
if [ "$STABILIZATION_SECONDS" -gt 0 ]; then
echo "候选槽稳定观察 ${STABILIZATION_SECONDS}s"
sleep "$STABILIZATION_SECONDS"
[ "$(container_health "$candidate")" = "healthy" ] || { cleanup_candidate; exit 1; }
fi
if ! switch_gateway_proxy; then
cleanup_candidate
exit 1
fi
worker_required=false
if needs_worker; then worker_required=true; fi
if [ "$worker_required" = true ] && [ "$active_slot" != legacy ]; then
if [ -n "$active_worker" ] && docker inspect "$active_worker" >/dev/null 2>&1; then
echo "暂停旧 Worker,避免新旧任务并行: $active_worker"
docker stop --time "$STOP_TIMEOUT" "$active_worker" >/dev/null
fi
if ! start_worker_candidate; then
[ -z "$active_worker" ] || docker start "$active_worker" >/dev/null 2>&1 || true
rollback_gateway_proxy || true
cleanup_candidate
exit 1
fi
fi
old_container_stopped=false
if [ -n "$active_container" ] && docker inspect "$active_container" >/dev/null 2>&1; then
if [ "$DRAIN_SECONDS" -gt 0 ]; then
echo "蓝绿切换后连接排空 ${DRAIN_SECONDS}s,旧 Web 实例保持运行: $active_container"
sleep "$DRAIN_SECONDS"
fi
echo "优雅停止旧 Web 实例: $active_container"
docker stop --time "$STOP_TIMEOUT" "$active_container" >/dev/null
old_container_stopped=true
fi
if [ "$worker_required" = true ] && [ "$active_slot" = legacy ]; then
if ! start_worker_candidate; then
[ "$old_container_stopped" = false ] || docker start "$active_container" >/dev/null 2>&1 || true
rollback_gateway_proxy || true
cleanup_candidate
exit 1
fi
fi
if ! reload_frontend_proxies; then
[ "$old_container_stopped" = false ] || docker start "$active_container" >/dev/null 2>&1 || true
if [ "$worker_required" = true ]; then
docker rm -f "$worker_candidate" >/dev/null 2>&1 || true
[ -z "$active_worker" ] || docker start "$active_worker" >/dev/null 2>&1 || true
fi
rollback_gateway_proxy || true
cleanup_candidate
reload_frontend_proxies || true
exit 1
fi
[ -z "$active_worker" ] || [ "$active_worker" = "$worker_candidate" ] \
|| docker rm "$active_worker" >/dev/null 2>&1 || true
[ "$old_container_stopped" = false ] || docker rm "$active_container" >/dev/null
if [ "$worker_required" = true ]; then
next_worker="$worker_candidate"
else
next_worker=""
fi
printf '%s|%s|%s|%s|%s\n' "$target_slot" "$candidate" "$target_port" "$IMAGE" "$next_worker" > "$STATE_FILE.tmp"
chmod 640 "$STATE_FILE.tmp"
mv -f "$STATE_FILE.tmp" "$STATE_FILE"
echo "蓝绿切换完成: service=$SERVICE active=$target_slot container=$candidate port=$target_port"
+38 -398
View File
@@ -28,177 +28,45 @@ for value_name in HEALTH_TIMEOUT_SECONDS HEALTH_POLL_SECONDS; do
esac
done
case "$DEPLOY_ENV" in
development|test|production) ;;
*)
echo "仅支持 development/test/production,当前: $DEPLOY_ENV" >&2
exit 2
;;
esac
if [ "$DEPLOY_ENV" = "development" ]; then
PUBLISHED_BIND_ADDRESS=127.0.0.1
else
PUBLISHED_BIND_ADDRESS="${PUBLISHED_BIND_ADDRESS:-0.0.0.0}"
fi
export PUBLISHED_BIND_ADDRESS
[ "$DEPLOY_ENV" = "test" ] || { echo "Only test is supported" >&2; exit 2; }
case "$SERVICE" in
*[!A-Za-z0-9_-]*|'')
echo "非法服务名: $SERVICE" >&2
exit 2
;;
gateway|auth|iam|firstleg|finance|order|warehouse|file|print|integration-service|lastleg-service|warehouse-overseas-service|audit-service) ;;
*) echo "Unknown or retired service: $SERVICE" >&2; exit 2 ;;
esac
if [ ! -f "$ENV_FILE" ]; then
echo "必需文件不存在: $ENV_FILE" >&2
exit 2
fi
env_file_has_key() {
grep -q "^$1=" "$ENV_FILE"
}
env_file_value() {
sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1 | tr -d '\r'
}
case "$SERVICE" in
gateway-overseas|lastleg-service|warehouse-overseas-service)
COMPOSE_FILE="deploy/docker-compose.overseas.yml"
PROJECT_NAME="firstleg-$DEPLOY_ENV-overseas"
;;
event-bridge-service)
BRIDGE_REGION="${EVENT_BRIDGE_REGION_OVERRIDE:-}"
if [ -z "$BRIDGE_REGION" ] && env_file_has_key REGION_CODE; then
BRIDGE_REGION="$(env_file_value REGION_CODE)"
fi
if [ -z "$BRIDGE_REGION" ]; then
echo "部署 event-bridge-service 时,环境文件必须显式包含 REGION_CODE=CN 或 REGION_CODE=OVERSEAS" >&2
exit 2
fi
case "$BRIDGE_REGION" in
CN) ;;
OVERSEAS)
COMPOSE_FILE="deploy/docker-compose.overseas.yml"
PROJECT_NAME="firstleg-$DEPLOY_ENV-overseas"
;;
*)
echo "部署 event-bridge-service 时,REGION_CODE 只能为 CN 或 OVERSEAS" >&2
exit 2
;;
esac
;;
esac
if [ "$SERVICE" = "gateway-overseas" ]; then
if ! env_file_has_key AUTH_SESSION_VALIDATION_URL || [ -z "$(env_file_value AUTH_SESSION_VALIDATION_URL)" ]; then
echo "部署 gateway-overseas 时,环境文件必须包含非空 AUTH_SESSION_VALIDATION_URL" >&2
exit 2
fi
fi
for file in "$ENV_FILE" "$COMPOSE_FILE"; do
if [ ! -f "$file" ]; then
echo "必需文件不存在: $file" >&2
exit 2
fi
[ "${TARGET_IS_LOCAL:-false}" = "true" ] || { echo "TEST deploy must run on the target host" >&2; exit 2; }
[ -f "$ENV_FILE" ] || { echo "Environment file missing" >&2; exit 2; }
env_file_value() { sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1 | tr -d '\r'; }
for key in NACOS_SERVER_ADDR NACOS_NAMESPACE NACOS_USERNAME NACOS_PASSWORD REDIS_HOST REDIS_PORT REDIS_DATABASE REDIS_TOKEN_KEY_PREFIX INTERNAL_SERVICE_TOKEN JWT_RSA_JWK_SET JWT_CURRENT_KID ROCKETMQ_NAME_SERVER; do
[ -n "$(env_file_value "$key")" ] || { echo "Missing $key" >&2; exit 2; }
done
if [ "$SERVICE" = "finance" ]; then
for key in FINANCE_DB_URL FINANCE_DB_USERNAME FINANCE_DB_PASSWORD; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 finance 服务时,环境文件必须包含非空 $key" >&2
exit 2
fi
[ "$(env_file_value MQ_TOPIC_PREFIX)" = "test-" ] || { echo "MQ_TOPIC_PREFIX must be test-" >&2; exit 2; }
case "$SERVICE" in
gateway|print|integration-service) db_prefix='' ;;
lastleg-service) db_prefix=LASTLEG ;;
warehouse-overseas-service) db_prefix=WAREHOUSE_OVERSEAS ;;
audit-service) db_prefix=AUDIT ;;
*) db_prefix=$(printf '%s' "$SERVICE" | tr '[:lower:]' '[:upper:]') ;;
esac
if [ -n "$db_prefix" ]; then
for suffix in URL USERNAME PASSWORD; do
key="${db_prefix}_DB_${suffix}"
[ -n "$(env_file_value "$key")" ] || { echo "Missing $key" >&2; exit 2; }
done
fi
if [ "$PROJECT_NAME" = "firstleg-$DEPLOY_ENV-overseas" ]; then
for key in NACOS_SERVER_ADDR OVERSEAS_NACOS_USERNAME OVERSEAS_NACOS_PASSWORD OVERSEAS_NACOS_NAMESPACE INTERNAL_SERVICE_TOKEN JWT_ISSUER OVERSEAS_JWT_AUDIENCE JWT_JWK_SET_URI; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署海外服务时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
fi
if [ "$SERVICE" = "firstleg" ]; then
if ! env_file_has_key LASTLEG_EVENT_BRIDGE_URL || [ -z "$(env_file_value LASTLEG_EVENT_BRIDGE_URL)" ]; then
echo "部署 firstleg 时,环境文件必须包含非空 LASTLEG_EVENT_BRIDGE_URL" >&2
exit 2
fi
if [ "$(env_file_value TERMINAL_DRAYAGE_MAIL_ENABLED)" = "true" ]; then
for key in TERMINAL_DRAYAGE_MAIL_HOST TERMINAL_DRAYAGE_MAIL_PORT TERMINAL_DRAYAGE_MAIL_USERNAME TERMINAL_DRAYAGE_MAIL_PASSWORD; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "启用码头车队邮件时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
fi
fi
if [ "$SERVICE" = "lastleg-service" ]; then
for key in LASTLEG_DB_URL LASTLEG_DB_USERNAME LASTLEG_DB_PASSWORD FIRSTLEG_EVENT_BRIDGE_URL DOMESTIC_WAREHOUSE_URL FILE_SERVICE_URL; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 lastleg-service 时,环境文件必须包含非空 $key" >&2
exit 2
fi
if [ "$SERVICE" = "firstleg" ] && [ "$(env_file_value TERMINAL_DRAYAGE_MAIL_ENABLED)" = "true" ]; then
for key in TERMINAL_DRAYAGE_MAIL_HOST TERMINAL_DRAYAGE_MAIL_PORT TERMINAL_DRAYAGE_MAIL_USERNAME TERMINAL_DRAYAGE_MAIL_PASSWORD; do
[ -n "$(env_file_value "$key")" ] || { echo "Missing $key" >&2; exit 2; }
done
fi
if [ "$SERVICE" = "warehouse-overseas-service" ]; then
for key in WAREHOUSE_OVERSEAS_DB_URL WAREHOUSE_OVERSEAS_DB_USERNAME WAREHOUSE_OVERSEAS_DB_PASSWORD; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 warehouse-overseas-service 时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
fi
if [ "$SERVICE" = "event-bridge-service" ]; then
if [ "$PROJECT_NAME" = "firstleg-$DEPLOY_ENV-overseas" ]; then
EVENT_BRIDGE_KEYS="EVENT_BRIDGE_OVERSEAS_DB_URL EVENT_BRIDGE_OVERSEAS_DB_USERNAME EVENT_BRIDGE_OVERSEAS_DB_PASSWORD EVENT_CONSUMER_URL"
else
EVENT_BRIDGE_KEYS="EVENT_BRIDGE_DB_URL EVENT_BRIDGE_DB_USERNAME EVENT_BRIDGE_DB_PASSWORD FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL"
fi
for key in $EVENT_BRIDGE_KEYS; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 event-bridge-service 时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
fi
if [ "$SERVICE" = "file" ]; then
if ! env_file_has_key ROCKETMQ_NAME_SERVER || [ -z "$(env_file_value ROCKETMQ_NAME_SERVER)" ]; then
echo "部署 file 服务时,环境文件必须包含非空 ROCKETMQ_NAME_SERVER" >&2
exit 2
fi
if ! env_file_has_key MQ_TOPIC_PREFIX; then
echo "部署 file 服务时,环境文件必须显式包含 MQ_TOPIC_PREFIX" >&2
exit 2
fi
MQ_PREFIX=$(env_file_value MQ_TOPIC_PREFIX)
if [ "$DEPLOY_ENV" = "test" ] && [ "$MQ_PREFIX" != "test-" ]; then
echo "测试环境部署 file 服务时 MQ_TOPIC_PREFIX 必须为 test-" >&2
exit 2
fi
if [ "$DEPLOY_ENV" = "development" ] && { [ -z "$MQ_PREFIX" ] || [ "$MQ_PREFIX" = "test-" ]; }; then
echo "开发环境部署 file 服务时 MQ_TOPIC_PREFIX 必须非空且不能使用 test-" >&2
exit 2
fi
if [ "$DEPLOY_ENV" = "production" ] && [ -n "$MQ_PREFIX" ]; then
echo "生产环境部署 file 服务时 MQ_TOPIC_PREFIX 必须为空,使用正式 Topic" >&2
exit 2
fi
fi
# Architecture/schema changes must use whole-release recovery, never an implicit old image.
export PUBLISHED_BIND_ADDRESS=127.0.0.1
: "${REGISTRY_USER:?REGISTRY_USER 未设置}"
: "${REGISTRY_PASSWORD:?REGISTRY_PASSWORD 未设置}"
local_registry_logout() {
docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true
if [ -n "${deploy_docker_config:-}" ]; then rm -rf -- "$deploy_docker_config"; fi
}
local_compose() {
@@ -255,8 +123,8 @@ wait_for_local_health() {
return 0
;;
no-health:running)
echo "容器未配置 HEALTHCHECK,以运行状态作为成功依据" >&2
return 0
echo "Missing HEALTHCHECK; deployment failed" >&2
return 1
;;
unhealthy|no-health:exited|no-health:dead)
return 1
@@ -270,6 +138,10 @@ wait_for_local_health() {
}
deploy_local() {
deploy_docker_config=$(mktemp -d)
DOCKER_CONFIG="$deploy_docker_config"
export DOCKER_CONFIG
trap local_registry_logout EXIT HUP INT TERM
printf '%s' "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "$REGISTRY_HOST"
trap local_registry_logout EXIT HUP INT TERM
previous_image="$(local_container_image || true)"
@@ -306,6 +178,11 @@ deploy_local() {
else
echo "新镜像容器不存在,无法读取启动日志" >&2
fi
if [ "${ALLOW_SAME_CONTRACT_ROLLBACK:-false}" != "true" ]; then
echo "Automatic rollback disabled: keep writes closed and recover the matched release" >&2
local_compose stop "$SERVICE" >/dev/null 2>&1 || true
return 1
fi
expected_prefix="$IMAGE_REPOSITORY/$SERVICE:"
case "$previous_image" in
"$expected_prefix"*)
@@ -346,241 +223,4 @@ deploy_local() {
return 1
}
if [ "${TARGET_IS_LOCAL:-false}" = "true" ]; then
echo "镜像已在目标服务器本机,直接部署: $IMAGE"
if [ "$DEPLOY_ENV" = "production" ]; then
printf '%s' "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "$REGISTRY_HOST"
trap local_registry_logout EXIT HUP INT TERM
docker pull "$IMAGE"
sh deploy/scripts/deploy-blue-green-service.sh \
"$DEPLOY_ENV" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE" "$ENV_FILE" "$COMPOSE_FILE" "$PROJECT_NAME"
local_registry_logout
trap - EXIT HUP INT TERM
exit 0
fi
deploy_local
exit 0
fi
SSH_TARGET="${TARGET_SSH_TARGET:?TARGET_SSH_TARGET 未设置}"
SSH_KEY="${TARGET_SSH_KEY:?TARGET_SSH_KEY 未设置}"
SSH_KNOWN_HOSTS="${TARGET_SSH_KNOWN_HOSTS:?TARGET_SSH_KNOWN_HOSTS 未设置}"
REMOTE_DIR="${TARGET_DEPLOY_DIR:?TARGET_DEPLOY_DIR 未设置}"
for file in "$SSH_KEY" "$SSH_KNOWN_HOSTS"; do
if [ ! -f "$file" ]; then
echo "SSH 文件不存在: $file" >&2
exit 2
fi
done
run_ssh() {
ssh -i "$SSH_KEY" \
-o BatchMode=yes \
-o Compression=no \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile="$SSH_KNOWN_HOSTS" \
-o ConnectTimeout=10 \
"$SSH_TARGET" "$@"
}
run_scp() {
scp -i "$SSH_KEY" \
-o BatchMode=yes \
-o Compression=no \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile="$SSH_KNOWN_HOSTS" \
-o ConnectTimeout=10 \
"$@"
}
run_ssh install -d -m 750 "$REMOTE_DIR"
SUFFIX="${IMAGE_TAG}.$$.${SERVICE}"
REMOTE_ENV_NAME=".env.$PROJECT_NAME"
REMOTE_COMPOSE_NAME="docker-compose.$PROJECT_NAME.yml"
REMOTE_BLUE_GREEN_NAME="deploy-blue-green-service.sh"
REMOTE_ENV_TMP="$REMOTE_DIR/$REMOTE_ENV_NAME.$SUFFIX.tmp"
REMOTE_COMPOSE_TMP="$REMOTE_DIR/$REMOTE_COMPOSE_NAME.$SUFFIX.tmp"
REMOTE_BLUE_GREEN_TMP="$REMOTE_DIR/$REMOTE_BLUE_GREEN_NAME.$SUFFIX.tmp"
run_scp "$ENV_FILE" "$SSH_TARGET:$REMOTE_ENV_TMP"
run_scp "$COMPOSE_FILE" "$SSH_TARGET:$REMOTE_COMPOSE_TMP"
run_scp "deploy/scripts/deploy-blue-green-service.sh" "$SSH_TARGET:$REMOTE_BLUE_GREEN_TMP"
run_ssh sh -s -- "$REMOTE_ENV_TMP" "$REMOTE_COMPOSE_TMP" "$REMOTE_BLUE_GREEN_TMP" \
"$REMOTE_DIR/$REMOTE_ENV_NAME" "$REMOTE_DIR/$REMOTE_COMPOSE_NAME" "$REMOTE_DIR/$REMOTE_BLUE_GREEN_NAME" <<'REMOTE_PREPARE'
set -eu
env_tmp="$1"
compose_tmp="$2"
blue_green_tmp="$3"
env_target="$4"
compose_target="$5"
blue_green_target="$6"
chmod 600 "$env_tmp"
chmod 644 "$compose_tmp"
chmod 750 "$blue_green_tmp"
mv -f "$env_tmp" "$env_target"
mv -f "$compose_tmp" "$compose_target"
mv -f "$blue_green_tmp" "$blue_green_target"
REMOTE_PREPARE
remote_registry_logout() {
run_ssh docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true
}
echo "目标服务器从共享仓库拉取增量镜像: $IMAGE"
printf '%s' "$REGISTRY_PASSWORD" | run_ssh docker login -u "$REGISTRY_USER" --password-stdin "$REGISTRY_HOST"
trap remote_registry_logout EXIT HUP INT TERM
run_ssh docker pull "$IMAGE"
if [ "$DEPLOY_ENV" = "production" ]; then
echo "$DEPLOY_ENV 服务使用蓝绿执行器,旧实例在候选槽健康前保持运行"
run_ssh sh "$REMOTE_DIR/$REMOTE_BLUE_GREEN_NAME" \
"$DEPLOY_ENV" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE" \
"$REMOTE_DIR/$REMOTE_ENV_NAME" "$REMOTE_DIR/$REMOTE_COMPOSE_NAME" "$PROJECT_NAME"
remote_registry_logout
trap - EXIT HUP INT TERM
exit 0
fi
run_ssh sh -s -- "$REMOTE_DIR" "$DEPLOY_ENV" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE" "$PROJECT_NAME" "$REMOTE_ENV_NAME" "$REMOTE_COMPOSE_NAME" "$HEALTH_TIMEOUT_SECONDS" "$HEALTH_POLL_SECONDS" <<'REMOTE_DEPLOY'
set -eu
remote_dir="$1"
deploy_env="$2"
image_repository="$3"
image_tag="$4"
service="$5"
project_name="$6"
env_file="$7"
compose_file="$8"
health_timeout_seconds="$9"
health_poll_seconds="${10}"
cd "$remote_dir"
export DEPLOY_ENV="$deploy_env"
export IMAGE_REPOSITORY="$image_repository"
export IMAGE_TAG="$image_tag"
if [ "$deploy_env" = "development" ]; then
export PUBLISHED_BIND_ADDRESS=127.0.0.1
else
export PUBLISHED_BIND_ADDRESS="${PUBLISHED_BIND_ADDRESS:-0.0.0.0}"
fi
compose() {
docker compose \
--project-name "$project_name" \
--env-file "$env_file" \
-f "$compose_file" \
"$@"
}
container_id() {
compose ps --all --quiet "$service" | head -n 1
}
container_image() {
id="$(container_id)"
if [ -n "$id" ]; then
docker inspect --format '{{.Config.Image}}' "$id"
fi
}
health_state() {
id="$(container_id)"
if [ -z "$id" ]; then
echo missing
return
fi
docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-health:{{.State.Status}}{{end}}' "$id" 2>/dev/null || echo missing
}
wait_for_health() {
elapsed=0
while [ "$elapsed" -lt "$health_timeout_seconds" ]; do
state="$(health_state)"
echo "容器健康状态: $state(已等待 ${elapsed}s)"
case "$state" in
healthy) return 0 ;;
no-health:running)
echo "容器未配置 HEALTHCHECK,以运行状态作为成功依据" >&2
return 0
;;
unhealthy|no-health:exited|no-health:dead) return 1 ;;
esac
sleep "$health_poll_seconds"
elapsed=$((elapsed + health_poll_seconds))
done
echo "等待容器健康超时: ${health_timeout_seconds}s" >&2
return 1
}
new_image="$image_repository/$service:$image_tag"
previous_image="$(container_image || true)"
if [ -n "$previous_image" ]; then
echo "部署前运行镜像: $previous_image"
else
echo "首次部署,未发现可回滚的旧镜像"
fi
new_started=true
if ! compose up -d --no-deps --pull never "$service"; then
echo "新镜像启动命令失败: $new_image" >&2
new_started=false
fi
if [ "$new_started" = "true" ] && wait_for_health; then
running_image="$(container_image || true)"
if [ "$running_image" != "$new_image" ]; then
echo "运行容器镜像与请求镜像不一致: expected=$new_image actual=${running_image:-missing}" >&2
exit 1
fi
echo "新镜像健康检查通过: $new_image"
compose ps "$service"
exit 0
fi
echo "新镜像健康检查失败: $new_image" >&2
failed_container_id="$(container_id || true)"
if [ -n "$failed_container_id" ]; then
echo "新镜像容器最近日志(最多 200 行):" >&2
docker logs --tail 200 "$failed_container_id" 2>&1 || true
else
echo "新镜像容器不存在,无法读取启动日志" >&2
fi
expected_prefix="$image_repository/$service:"
case "$previous_image" in
"$expected_prefix"*) previous_tag="${previous_image#"$expected_prefix"}" ;;
'')
echo "首次部署没有可回滚镜像,部署失败" >&2
compose stop "$service" >/dev/null 2>&1 || true
exit 1
;;
*)
echo "旧镜像不属于当前服务,拒绝自动回滚: $previous_image" >&2
exit 1
;;
esac
if [ "$previous_image" = "$new_image" ]; then
echo "旧镜像与新镜像相同,无法自动回滚: $new_image" >&2
exit 1
fi
echo "开始自动回滚到: $previous_image" >&2
if ! docker pull "$previous_image"; then
echo "自动回滚失败,无法拉取旧镜像: $previous_image" >&2
exit 1
fi
IMAGE_TAG="$previous_tag"
export IMAGE_TAG
if ! compose up -d --no-deps --pull never "$service"; then
echo "自动回滚失败,旧镜像启动命令失败: $previous_image" >&2
exit 1
fi
if wait_for_health; then
echo "自动回滚成功: $previous_image" >&2
compose ps "$service" || true
else
echo "自动回滚失败,旧镜像未恢复健康: $previous_image" >&2
fi
exit 1
REMOTE_DEPLOY
remote_registry_logout
trap - EXIT HUP INT TERM
deploy_local
@@ -1,160 +0,0 @@
#!/usr/bin/env sh
set -eu
if [ "$#" -ne 4 ]; then
echo "用法: $0 <镜像仓库> <镜像标签> <逗号分隔服务> <环境文件>" >&2
exit 2
fi
IMAGE_REPOSITORY="$1"
IMAGE_TAG="$2"
SERVICE_CSV="$3"
ENV_FILE="$4"
SSH_TARGET="${DEVELOPMENT_SSH_TARGET:-deploy@172.31.198.8}"
SSH_KEY="${DEVELOPMENT_SSH_KEY:-/var/jenkins_home/.ssh/dev_deploy}"
SSH_KNOWN_HOSTS="${DEVELOPMENT_SSH_KNOWN_HOSTS:-/var/jenkins_home/.ssh/known_hosts}"
REMOTE_DIR="${DEVELOPMENT_DEPLOY_DIR:-/opt/firstleg/application}"
COMPOSE_FILE="${DEPLOY_COMPOSE_FILE:-deploy/docker-compose.yml}"
PROJECT_NAME="${DEPLOY_PROJECT_NAME:-firstleg-development}"
if [ ! -f "$SSH_KEY" ]; then
echo "开发环境 SSH 私钥不存在: $SSH_KEY" >&2
exit 2
fi
if [ ! -f "$SSH_KNOWN_HOSTS" ]; then
echo "SSH 主机指纹文件不存在: $SSH_KNOWN_HOSTS" >&2
exit 2
fi
if [ ! -f "$ENV_FILE" ]; then
echo "开发环境变量文件不存在: $ENV_FILE" >&2
exit 2
fi
if [ ! -f "$COMPOSE_FILE" ]; then
echo "Compose 文件不存在: $COMPOSE_FILE" >&2
exit 2
fi
SERVICES=$(printf '%s' "$SERVICE_CSV" | tr ',' ' ')
for service in $SERVICES; do
case "$service" in
*[!A-Za-z0-9_-]*|'')
echo "非法服务名: $service" >&2
exit 2
;;
esac
done
if [ "${DEVELOPMENT_DEPLOY_LOCAL:-false}" = "true" ]; then
echo "开发镜像已在开发节点本机构建,直接部署: $SERVICE_CSV"
install -d -m 750 "$REMOTE_DIR"
SUFFIX="${IMAGE_TAG}.$$.${SERVICE_CSV}"
LOCAL_ENV_TMP="$REMOTE_DIR/.env.$SUFFIX.tmp"
LOCAL_COMPOSE_TMP="$REMOTE_DIR/docker-compose.yml.$SUFFIX.tmp"
install -m 600 "$ENV_FILE" "$LOCAL_ENV_TMP"
install -m 644 "$COMPOSE_FILE" "$LOCAL_COMPOSE_TMP"
mv -f "$LOCAL_ENV_TMP" "$REMOTE_DIR/.env"
mv -f "$LOCAL_COMPOSE_TMP" "$REMOTE_DIR/docker-compose.yml"
for service in $SERVICES; do
docker image inspect "$IMAGE_REPOSITORY/$service:$IMAGE_TAG" >/dev/null
done
cd "$REMOTE_DIR"
export DEPLOY_ENV=development
export IMAGE_REPOSITORY IMAGE_TAG
export PUBLISHED_BIND_ADDRESS=127.0.0.1
docker compose \
--project-name "$PROJECT_NAME" \
--env-file .env \
-f docker-compose.yml \
pull $SERVICES
docker compose \
--project-name "$PROJECT_NAME" \
--env-file .env \
-f docker-compose.yml \
up -d --remove-orphans --pull never $SERVICES
docker compose \
--project-name "$PROJECT_NAME" \
--env-file .env \
-f docker-compose.yml \
ps $SERVICES
exit 0
fi
run_ssh() {
ssh -i "$SSH_KEY" \
-o BatchMode=yes \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile="$SSH_KNOWN_HOSTS" \
-o ConnectTimeout=10 \
"$SSH_TARGET" "$@"
}
run_scp() {
scp -i "$SSH_KEY" \
-o BatchMode=yes \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile="$SSH_KNOWN_HOSTS" \
-o ConnectTimeout=10 \
"$@"
}
run_ssh install -d -m 750 "$REMOTE_DIR"
SUFFIX="${IMAGE_TAG}.$$.${SERVICE_CSV}"
REMOTE_ENV_TMP="$REMOTE_DIR/.env.$SUFFIX.tmp"
REMOTE_COMPOSE_TMP="$REMOTE_DIR/docker-compose.yml.$SUFFIX.tmp"
run_scp "$ENV_FILE" "$SSH_TARGET:$REMOTE_ENV_TMP"
run_scp "$COMPOSE_FILE" "$SSH_TARGET:$REMOTE_COMPOSE_TMP"
run_ssh sh -s -- "$REMOTE_DIR" "$REMOTE_ENV_TMP" "$REMOTE_COMPOSE_TMP" <<'REMOTE_PREPARE'
set -eu
remote_dir="$1"
env_tmp="$2"
compose_tmp="$3"
chmod 600 "$env_tmp"
chmod 644 "$compose_tmp"
mv -f "$env_tmp" "$remote_dir/.env"
mv -f "$compose_tmp" "$remote_dir/docker-compose.yml"
REMOTE_PREPARE
for service in $SERVICES; do
image="$IMAGE_REPOSITORY/$service:$IMAGE_TAG"
docker image inspect "$image" >/dev/null
echo "传输开发镜像: $image"
docker save "$image" | gzip -1 | run_ssh 'gzip -dc | docker load'
done
run_ssh sh -s -- "$REMOTE_DIR" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE_CSV" "$PROJECT_NAME" <<'REMOTE_DEPLOY'
set -eu
remote_dir="$1"
image_repository="$2"
image_tag="$3"
service_csv="$4"
project_name="$5"
services=$(printf '%s' "$service_csv" | tr ',' ' ')
cd "$remote_dir"
export DEPLOY_ENV=development
export IMAGE_REPOSITORY="$image_repository"
export IMAGE_TAG="$image_tag"
export PUBLISHED_BIND_ADDRESS=127.0.0.1
docker compose \
--project-name "$project_name" \
--env-file .env \
-f docker-compose.yml \
up -d --remove-orphans --pull never $services
docker compose \
--project-name "$project_name" \
--env-file .env \
-f docker-compose.yml \
ps $services
REMOTE_DEPLOY
+4 -222
View File
@@ -1,225 +1,7 @@
#!/usr/bin/env sh
set -eu
if [ "$#" -ne 5 ]; then
echo "用法: $0 <环境> <镜像仓库> <镜像标签> <逗号分隔服务> <环境文件>" >&2
exit 2
fi
DEPLOY_ENV="$1"
IMAGE_REPOSITORY="$2"
IMAGE_TAG="$3"
SERVICE_CSV="$4"
ENV_FILE="$5"
if [ ! -f "$ENV_FILE" ]; then
echo "环境文件不存在: $ENV_FILE" >&2
exit 2
fi
SERVICES=$(printf '%s' "$SERVICE_CSV" | tr ',' ' ')
for service in $SERVICES; do
case "$service" in
*[!A-Za-z0-9_-]*|'')
echo "非法服务名: $service" >&2
exit 2
;;
esac
[ "$#" -eq 5 ] || { echo "Usage: $0 test repository tag service-csv env-file" >&2; exit 2; }
[ "$1" = "test" ] || exit 2
for service in $(printf '%s' "$4" | tr ',' ' '); do
sh deploy/scripts/deploy-built-service.sh "$1" "$2" "$3" "$service" "$5"
done
env_file_has_key() {
grep -q "^$1=" "$ENV_FILE"
}
env_file_value() {
sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1 | tr -d '\r'
}
DEPLOY_REGION=CN
for service in $SERVICES; do
case "$service" in
gateway-overseas|lastleg-service|warehouse-overseas-service) DEPLOY_REGION=OVERSEAS ;;
event-bridge-service)
BRIDGE_REGION="${EVENT_BRIDGE_REGION_OVERRIDE:-}"
if [ -z "$BRIDGE_REGION" ] && env_file_has_key REGION_CODE; then
BRIDGE_REGION="$(env_file_value REGION_CODE)"
fi
if [ -z "$BRIDGE_REGION" ]; then
echo "部署 event-bridge-service 时,环境文件必须显式包含 REGION_CODE=CN 或 REGION_CODE=OVERSEAS" >&2
exit 2
fi
case "$BRIDGE_REGION" in
CN) ;;
OVERSEAS) DEPLOY_REGION=OVERSEAS ;;
*) echo "部署 event-bridge-service 时,REGION_CODE 只能为 CN 或 OVERSEAS" >&2; exit 2 ;;
esac
;;
esac
done
if [ "$DEPLOY_REGION" = "OVERSEAS" ]; then
for service in $SERVICES; do
case "$service" in
gateway-overseas|lastleg-service|warehouse-overseas-service|event-bridge-service) ;;
*) echo "一次部署不得混合国内与海外服务: $SERVICE_CSV" >&2; exit 2 ;;
esac
done
COMPOSE_FILE="deploy/docker-compose.overseas.yml"
PROJECT_NAME="firstleg-${DEPLOY_ENV}-overseas"
else
COMPOSE_FILE="deploy/docker-compose.yml"
PROJECT_NAME="firstleg-${DEPLOY_ENV}"
fi
if [ "$DEPLOY_REGION" = "OVERSEAS" ]; then
for key in NACOS_SERVER_ADDR OVERSEAS_NACOS_USERNAME OVERSEAS_NACOS_PASSWORD OVERSEAS_NACOS_NAMESPACE INTERNAL_SERVICE_TOKEN JWT_ISSUER OVERSEAS_JWT_AUDIENCE JWT_JWK_SET_URI; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署海外服务时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
fi
case " $SERVICES " in
*" gateway-overseas "*)
if ! env_file_has_key AUTH_SESSION_VALIDATION_URL || [ -z "$(env_file_value AUTH_SESSION_VALIDATION_URL)" ]; then
echo "部署 gateway-overseas 时,环境文件必须包含非空 AUTH_SESSION_VALIDATION_URL" >&2
exit 2
fi
;;
esac
case " $SERVICES " in
*" finance "*)
for key in FINANCE_DB_URL FINANCE_DB_USERNAME FINANCE_DB_PASSWORD; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 finance 服务时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
;;
esac
case " $SERVICES " in
*" firstleg "*)
if ! env_file_has_key LASTLEG_EVENT_BRIDGE_URL || [ -z "$(env_file_value LASTLEG_EVENT_BRIDGE_URL)" ]; then
echo "部署 firstleg 时,环境文件必须包含非空 LASTLEG_EVENT_BRIDGE_URL" >&2
exit 2
fi
;;
esac
case " $SERVICES " in
*" lastleg-service "*)
for key in LASTLEG_DB_URL LASTLEG_DB_USERNAME LASTLEG_DB_PASSWORD FIRSTLEG_EVENT_BRIDGE_URL DOMESTIC_WAREHOUSE_URL FILE_SERVICE_URL; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 lastleg-service 时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
;;
esac
case " $SERVICES " in
*" warehouse-overseas-service "*)
for key in WAREHOUSE_OVERSEAS_DB_URL WAREHOUSE_OVERSEAS_DB_USERNAME WAREHOUSE_OVERSEAS_DB_PASSWORD; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 warehouse-overseas-service 时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
;;
esac
case " $SERVICES " in
*" event-bridge-service "*)
if [ "$DEPLOY_REGION" = "OVERSEAS" ]; then
EVENT_BRIDGE_KEYS="EVENT_BRIDGE_OVERSEAS_DB_URL EVENT_BRIDGE_OVERSEAS_DB_USERNAME EVENT_BRIDGE_OVERSEAS_DB_PASSWORD EVENT_CONSUMER_URL"
else
EVENT_BRIDGE_KEYS="EVENT_BRIDGE_DB_URL EVENT_BRIDGE_DB_USERNAME EVENT_BRIDGE_DB_PASSWORD FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL"
fi
for key in $EVENT_BRIDGE_KEYS; do
if ! env_file_has_key "$key" || [ -z "$(env_file_value "$key")" ]; then
echo "部署 event-bridge-service 时,环境文件必须包含非空 $key" >&2
exit 2
fi
done
;;
esac
case " $SERVICES " in
*" file "*)
if ! env_file_has_key ROCKETMQ_NAME_SERVER || [ -z "$(env_file_value ROCKETMQ_NAME_SERVER)" ]; then
echo "部署 file 服务时,环境文件必须包含非空 ROCKETMQ_NAME_SERVER" >&2
exit 2
fi
if ! env_file_has_key MQ_TOPIC_PREFIX; then
echo "部署 file 服务时,环境文件必须显式包含 MQ_TOPIC_PREFIX:测试填 test-,生产填空字符串" >&2
exit 2
fi
MQ_PREFIX=$(env_file_value MQ_TOPIC_PREFIX)
case "$DEPLOY_ENV" in
test)
if [ "$MQ_PREFIX" != "test-" ]; then
echo "测试环境部署 file 服务时 MQ_TOPIC_PREFIX 必须为 test-" >&2
exit 2
fi
;;
production)
if [ -n "$MQ_PREFIX" ]; then
echo "生产环境部署 file 服务时 MQ_TOPIC_PREFIX 必须显式为空字符串" >&2
exit 2
fi
;;
*)
if [ -z "$MQ_PREFIX" ] || [ "$MQ_PREFIX" = "test-" ]; then
echo "预发布环境部署 file 服务时 MQ_TOPIC_PREFIX 必须为非空且不能使用 test-" >&2
exit 2
fi
;;
esac
;;
esac
export DEPLOY_ENV IMAGE_REPOSITORY IMAGE_TAG
if [ "$DEPLOY_ENV" = "development" ]; then
DEPLOY_COMPOSE_FILE="$COMPOSE_FILE" DEPLOY_PROJECT_NAME="$PROJECT_NAME" exec sh deploy/scripts/deploy-development-service.sh \
"$IMAGE_REPOSITORY" "$IMAGE_TAG" "$SERVICE_CSV" "$ENV_FILE"
fi
if [ "$DEPLOY_ENV" = "test" ] || [ "$DEPLOY_ENV" = "production" ]; then
docker compose \
--project-name "$PROJECT_NAME" \
--env-file "$ENV_FILE" \
-f "$COMPOSE_FILE" \
pull $SERVICES
for service in $SERVICES; do
if [ "$DEPLOY_ENV" = test ]; then
docker compose --project-name "$PROJECT_NAME" --env-file "$ENV_FILE" -f "$COMPOSE_FILE" \
up -d --no-deps --pull never "$service"
continue
fi
sh deploy/scripts/deploy-blue-green-service.sh \
"$DEPLOY_ENV" "$IMAGE_REPOSITORY" "$IMAGE_TAG" "$service" "$ENV_FILE" "$COMPOSE_FILE" "$PROJECT_NAME"
done
exit 0
fi
docker compose \
--project-name "$PROJECT_NAME" \
--env-file "$ENV_FILE" \
-f "$COMPOSE_FILE" \
pull $SERVICES
docker compose \
--project-name "$PROJECT_NAME" \
--env-file "$ENV_FILE" \
-f "$COMPOSE_FILE" \
up -d --remove-orphans --pull never $SERVICES
docker compose \
--project-name "$PROJECT_NAME" \
--env-file "$ENV_FILE" \
-f "$COMPOSE_FILE" \
ps $SERVICES
+12 -44
View File
@@ -1,47 +1,15 @@
#!/usr/bin/env sh
set -eu
# 仅用于测试基础设施容器。所有 SQL 均使用 CREATE ... IF NOT EXISTS 或幂等扩展语句,
# 不删除已有业务数据;仓库样例数据使用固定 ID 重复写入。
MYSQL_CONTAINER="${TEST_MYSQL_CONTAINER:-firstleg-test-mysql}"
if ! docker inspect --type container "$MYSQL_CONTAINER" >/dev/null 2>&1; then
echo "测试 MySQL 容器不存在:$MYSQL_CONTAINER" >&2
echo "请先执行 docker compose -f docker-compose-infra.yml up -d" >&2
exit 1
fi
if [ "$(docker inspect -f '{{.State.Running}}' "$MYSQL_CONTAINER")" != "true" ]; then
echo "测试 MySQL 容器未运行:$MYSQL_CONTAINER" >&2
exit 1
fi
for sql_file in \
auth/src/main/resources/db/auth.sql \
iam/src/main/resources/db/iam.sql \
auth/src/main/resources/db/phase1-admin-accounts.sql \
iam/src/main/resources/db/phase1-admin-rbac.sql \
iam/src/main/resources/db/phase1-onboarding.sql \
iam/src/main/resources/db/phase1-customer-profile.sql \
iam/src/main/resources/db/phase1-member-permissions.sql \
iam/src/main/resources/db/rbac-role-management-v2.sql \
iam/src/main/resources/db/phase1-platform-master-data-and-permission-management.sql \
order/src/main/resources/db/order.sql \
order/src/main/resources/db/phase1-order-extension.sql \
order/src/main/resources/db/phase1-masterdata-extension.sql \
warehouse/src/main/resources/db/warehouse.sql \
warehouse/src/main/resources/db/warehouse-partner.sql \
warehouse/src/main/resources/db/warehouse-express-company.sql \
warehouse/src/main/resources/db/phase1-warehouse-schema-migration.sql \
warehouse/src/main/resources/db/warehouse-seed.sql \
file/src/main/resources/db/file.sql
do
if [ ! -f "$sql_file" ]; then
echo "找不到初始化 SQL:$sql_file" >&2
exit 1
fi
echo "初始化测试数据库:$sql_file"
docker exec -i "$MYSQL_CONTAINER" sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --default-character-set=utf8mb4' < "$sql_file"
# Empty-schema baseline only. Never invoked by Compose/Jenkins automatically.
[ "${ALLOW_TEST_SCHEMA_INITIALIZATION:-false}" = "true" ] || { echo 'Explicit TEST SQL authorization required' >&2; exit 2; }
: "${TEST_MYSQL_CONTAINER:?Specify the authorized TEST MySQL container}"
[ "$#" -eq 2 ] || { echo 'Usage: initialize-test-databases.sh <firstleg-baseline.sql> <overseas-baseline.sql>' >&2; exit 2; }
for sql_file in "$1" "$2"; do
[ -f "$sql_file" ] || { echo "Missing reviewed baseline: $sql_file" >&2; exit 2; }
done
[ "$1" != "$2" ] || { echo 'Two distinct reviewed baselines required' >&2; exit 2; }
[ "$(docker inspect -f '{{.State.Running}}' "$TEST_MYSQL_CONTAINER")" = 'true' ] || { echo 'TEST MySQL is not running' >&2; exit 1; }
for sql_file in "$1" "$2"; do
echo "Applying reviewed TEST baseline: $sql_file"
docker exec -i "$TEST_MYSQL_CONTAINER" sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --default-character-set=utf8mb4' < "$sql_file"
done
echo "测试数据库初始化完成。"