feat: harden deployment scripts and add tenant migration
Issues found during P5-01 acceptance testing on WSL2: configure.sh: sync MySQL/Redis host/port into config/xuqm.env (was only writing to .env, leaving xuqm.env with hardcoded 127.0.0.1). install.sh: add docker login step before compose up; reads REGISTRY_USER/REGISTRY_PASSWORD from .env; --skip-registry-login flag for offline bundles or pre-authenticated environments. healthcheck.sh: move docs-site from required to optional container list (image may not exist in all ACR namespaces); add localhost fallback URL for actuator/health when CONSOLE_DOMAIN is not set; add PRIVATE mode verification via /api/private/deployment/status. scripts/migrate-tenant.sh (new): migrates a single tenant from the public platform MySQL to the private deployment. Exports t_tenant, t_app, t_feature_service with explicit column names to survive schema-order differences; supports --dry-run, --reset-password, managed/external destination MySQL, and restarts tenant-service after applying. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
20423a0347
当前提交
f6189a5283
@ -110,7 +110,7 @@ TENANT_BOOTSTRAP_APP_KEY="$(ask "App key" "${TENANT_BOOTSTRAP_APP_KEY:-ak_privat
|
|||||||
REGISTRY="$(ask "Docker registry" "${REGISTRY:-registry.example.com/xuqm}" "registry.example.com/xuqm")"
|
REGISTRY="$(ask "Docker registry" "${REGISTRY:-registry.example.com/xuqm}" "registry.example.com/xuqm")"
|
||||||
IMAGE_TAG="$(ask "Image tag" "${IMAGE_TAG:-$(cat "$ROOT_DIR/VERSION" | tr -d '[:space:]')}" "")"
|
IMAGE_TAG="$(ask "Image tag" "${IMAGE_TAG:-$(cat "$ROOT_DIR/VERSION" | tr -d '[:space:]')}" "")"
|
||||||
|
|
||||||
# Write .env (idempotent — only update values that changed)
|
# Write .env (compose variable substitution — used by ${VAR} in docker-compose.yml)
|
||||||
set_env_value "$ROOT_DIR/.env" "CONSOLE_DOMAIN" "$CONSOLE_DOMAIN"
|
set_env_value "$ROOT_DIR/.env" "CONSOLE_DOMAIN" "$CONSOLE_DOMAIN"
|
||||||
set_env_value "$ROOT_DIR/.env" "OPS_DOMAIN" "$OPS_DOMAIN"
|
set_env_value "$ROOT_DIR/.env" "OPS_DOMAIN" "$OPS_DOMAIN"
|
||||||
set_env_value "$ROOT_DIR/.env" "DOCS_DOMAIN" "$DOCS_DOMAIN"
|
set_env_value "$ROOT_DIR/.env" "DOCS_DOMAIN" "$DOCS_DOMAIN"
|
||||||
@ -138,6 +138,26 @@ set_env_value "$ROOT_DIR/.env" "IMAGE_TAG" "$IMAGE_TAG"
|
|||||||
set_env_value "$ROOT_DIR/.env" "TENANT_BOOTSTRAP_EMAIL" "$TENANT_BOOTSTRAP_EMAIL"
|
set_env_value "$ROOT_DIR/.env" "TENANT_BOOTSTRAP_EMAIL" "$TENANT_BOOTSTRAP_EMAIL"
|
||||||
set_env_value "$ROOT_DIR/.env" "TENANT_BOOTSTRAP_APP_KEY" "$TENANT_BOOTSTRAP_APP_KEY"
|
set_env_value "$ROOT_DIR/.env" "TENANT_BOOTSTRAP_APP_KEY" "$TENANT_BOOTSTRAP_APP_KEY"
|
||||||
|
|
||||||
|
# Sync infra settings into xuqm.env so containers receive correct host/port values
|
||||||
|
# via env_file:, consistent with the SPRING_DATASOURCE_* overrides in docker-compose.yml.
|
||||||
|
XUQM_ENV="$ROOT_DIR/config/xuqm.env"
|
||||||
|
set_env_value "$XUQM_ENV" "MYSQL_HOST" "$MYSQL_HOST"
|
||||||
|
set_env_value "$XUQM_ENV" "MYSQL_PORT" "$MYSQL_PORT"
|
||||||
|
set_env_value "$XUQM_ENV" "MYSQL_DATABASE" "$MYSQL_DATABASE"
|
||||||
|
set_env_value "$XUQM_ENV" "MYSQL_USERNAME" "$MYSQL_USERNAME"
|
||||||
|
set_env_value "$XUQM_ENV" "REDIS_HOST" "$REDIS_HOST"
|
||||||
|
set_env_value "$XUQM_ENV" "REDIS_PORT" "$REDIS_PORT"
|
||||||
|
set_env_value "$XUQM_ENV" "CONSOLE_DOMAIN" "$CONSOLE_DOMAIN"
|
||||||
|
[ -n "${FILE_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "FILE_DOMAIN" "$FILE_DOMAIN"
|
||||||
|
[ -n "${IM_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "IM_DOMAIN" "$IM_DOMAIN"
|
||||||
|
[ -n "${PUSH_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "PUSH_DOMAIN" "$PUSH_DOMAIN"
|
||||||
|
[ -n "${UPDATE_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "UPDATE_DOMAIN" "$UPDATE_DOMAIN"
|
||||||
|
[ -n "${LICENSE_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "LICENSE_DOMAIN" "$LICENSE_DOMAIN"
|
||||||
|
set_env_value "$XUQM_ENV" "ENABLE_IM" "$ENABLE_IM"
|
||||||
|
set_env_value "$XUQM_ENV" "ENABLE_PUSH" "$ENABLE_PUSH"
|
||||||
|
set_env_value "$XUQM_ENV" "ENABLE_UPDATE" "$ENABLE_UPDATE"
|
||||||
|
set_env_value "$XUQM_ENV" "ENABLE_LICENSE" "$ENABLE_LICENSE"
|
||||||
|
|
||||||
# Write secrets.env
|
# Write secrets.env
|
||||||
set_env_value "$ROOT_DIR/config/secrets.env" "MYSQL_PASSWORD" "$MYSQL_PASSWORD"
|
set_env_value "$ROOT_DIR/config/secrets.env" "MYSQL_PASSWORD" "$MYSQL_PASSWORD"
|
||||||
[ -n "${MYSQL_ROOT_PASSWORD:-}" ] && \
|
[ -n "${MYSQL_ROOT_PASSWORD:-}" ] && \
|
||||||
|
|||||||
@ -24,8 +24,8 @@ else
|
|||||||
check_fail "docker" "daemon not available"
|
check_fail "docker" "daemon not available"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Container states
|
# Container states — docs-site is optional (image may not exist in all registries)
|
||||||
for svc in tenant-service file-service tenant-web ops-web docs-site nginx; do
|
for svc in tenant-service file-service tenant-web ops-web nginx; do
|
||||||
STATE="$(compose ps -q "$svc" 2>/dev/null | head -1 || true)"
|
STATE="$(compose ps -q "$svc" 2>/dev/null | head -1 || true)"
|
||||||
if [ -n "$STATE" ]; then
|
if [ -n "$STATE" ]; then
|
||||||
STATUS="$(docker inspect --format='{{.State.Status}}' "$STATE" 2>/dev/null || echo 'unknown')"
|
STATUS="$(docker inspect --format='{{.State.Status}}' "$STATE" 2>/dev/null || echo 'unknown')"
|
||||||
@ -39,8 +39,8 @@ for svc in tenant-service file-service tenant-web ops-web docs-site nginx; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Optional service containers
|
# Optional service containers (docs-site included here — warn only if present but unhealthy)
|
||||||
for svc in im-service push-service update-service license-service; do
|
for svc in docs-site im-service push-service update-service license-service; do
|
||||||
STATE="$(compose ps -q "$svc" 2>/dev/null | head -1 || true)"
|
STATE="$(compose ps -q "$svc" 2>/dev/null | head -1 || true)"
|
||||||
if [ -n "$STATE" ]; then
|
if [ -n "$STATE" ]; then
|
||||||
STATUS="$(docker inspect --format='{{.State.Status}}' "$STATE" 2>/dev/null || echo 'unknown')"
|
STATUS="$(docker inspect --format='{{.State.Status}}' "$STATE" 2>/dev/null || echo 'unknown')"
|
||||||
@ -120,9 +120,29 @@ http_check() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Resolve the base URL to probe: prefer CONSOLE_DOMAIN, fall back to localhost
|
||||||
CONSOLE_DOMAIN_VAL="${CONSOLE_DOMAIN:-}"
|
CONSOLE_DOMAIN_VAL="${CONSOLE_DOMAIN:-}"
|
||||||
[ -n "$CONSOLE_DOMAIN_VAL" ] && http_check "http.tenant-service" "${CONSOLE_DOMAIN_VAL}/actuator/health"
|
if [ -n "$CONSOLE_DOMAIN_VAL" ]; then
|
||||||
[ -n "$CONSOLE_DOMAIN_VAL" ] && http_check "http.tenant-web" "${CONSOLE_DOMAIN_VAL}/"
|
HTTP_BASE="$CONSOLE_DOMAIN_VAL"
|
||||||
|
else
|
||||||
|
# Derive port from nginx container if running
|
||||||
|
NGINX_PORT="$(compose port nginx 80 2>/dev/null | cut -d: -f2 || true)"
|
||||||
|
HTTP_BASE="http://localhost:${NGINX_PORT:-80}"
|
||||||
|
fi
|
||||||
|
http_check "http.actuator" "${HTTP_BASE}/actuator/health"
|
||||||
|
http_check "http.tenant-web" "${HTTP_BASE}/"
|
||||||
|
|
||||||
|
# Verify private mode is active
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
DEPLOY_STATUS="$(curl -skL --max-time 5 "${HTTP_BASE}/api/private/deployment/status" 2>/dev/null || true)"
|
||||||
|
if printf '%s' "$DEPLOY_STATUS" | grep -q '"mode":"PRIVATE"'; then
|
||||||
|
check_pass "private-mode" "PRIVATE mode confirmed"
|
||||||
|
elif printf '%s' "$DEPLOY_STATUS" | grep -q '"mode"'; then
|
||||||
|
check_warn "private-mode" "deployment/status returned non-PRIVATE mode"
|
||||||
|
else
|
||||||
|
check_warn "private-mode" "deployment/status unreachable or unexpected response"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
[ "${ENABLE_IM:-false}" = "true" ] && [ -n "${IM_DOMAIN:-}" ] && \
|
[ "${ENABLE_IM:-false}" = "true" ] && [ -n "${IM_DOMAIN:-}" ] && \
|
||||||
http_check "http.im-service" "${IM_DOMAIN}/actuator/health"
|
http_check "http.im-service" "${IM_DOMAIN}/actuator/health"
|
||||||
|
|||||||
@ -13,6 +13,7 @@ load_env
|
|||||||
|
|
||||||
PROFILE="${COMPOSE_PROFILES:-base}"
|
PROFILE="${COMPOSE_PROFILES:-base}"
|
||||||
OFFLINE_BUNDLE=""
|
OFFLINE_BUNDLE=""
|
||||||
|
SKIP_LOGIN=false
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
@ -23,12 +24,17 @@ Options:
|
|||||||
--mysql-mode <mode> external or managed
|
--mysql-mode <mode> external or managed
|
||||||
--redis-mode <mode> external or managed
|
--redis-mode <mode> external or managed
|
||||||
--offline-bundle <path> Load images from an offline bundle before deployment
|
--offline-bundle <path> Load images from an offline bundle before deployment
|
||||||
|
--skip-registry-login Skip docker login (use when already logged in or in offline mode)
|
||||||
-h, --help Show help
|
-h, --help Show help
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
while [ "$#" -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
--skip-registry-login)
|
||||||
|
SKIP_LOGIN=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
--profile)
|
--profile)
|
||||||
PROFILE="${2:-}"
|
PROFILE="${2:-}"
|
||||||
shift 2
|
shift 2
|
||||||
@ -90,6 +96,22 @@ require_cmd docker
|
|||||||
if [ -n "$OFFLINE_BUNDLE" ]; then
|
if [ -n "$OFFLINE_BUNDLE" ]; then
|
||||||
[ -f "$OFFLINE_BUNDLE" ] || fail_json "XUQM_PRIVATE_4004" "offline bundle not found: $OFFLINE_BUNDLE" "install"
|
[ -f "$OFFLINE_BUNDLE" ] || fail_json "XUQM_PRIVATE_4004" "offline bundle not found: $OFFLINE_BUNDLE" "install"
|
||||||
docker load -i "$OFFLINE_BUNDLE"
|
docker load -i "$OFFLINE_BUNDLE"
|
||||||
|
SKIP_LOGIN=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Login to image registry (skip for offline bundles or when already authenticated)
|
||||||
|
if [ "$SKIP_LOGIN" = "false" ] && [ -n "${REGISTRY:-}" ]; then
|
||||||
|
REGISTRY_HOST="$(printf '%s' "$REGISTRY" | cut -d/ -f1)"
|
||||||
|
REGISTRY_USER="${REGISTRY_USER:-}"
|
||||||
|
REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-}"
|
||||||
|
if [ -n "$REGISTRY_USER" ] && [ -n "$REGISTRY_PASSWORD" ]; then
|
||||||
|
printf '%s' "$REGISTRY_PASSWORD" | \
|
||||||
|
docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin \
|
||||||
|
|| fail_json "XUQM_PRIVATE_4005" "docker login failed for $REGISTRY_HOST" "install"
|
||||||
|
audit "install" "REGISTRY_LOGIN_OK" "host=$REGISTRY_HOST"
|
||||||
|
else
|
||||||
|
audit "install" "REGISTRY_LOGIN_SKIPPED" "REGISTRY_USER/REGISTRY_PASSWORD not set; assuming pre-authenticated"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${MYSQL_MODE:-external}" = "managed" ]; then
|
if [ "${MYSQL_MODE:-external}" = "managed" ]; then
|
||||||
|
|||||||
332
scripts/migrate-tenant.sh
可执行文件
332
scripts/migrate-tenant.sh
可执行文件
@ -0,0 +1,332 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# migrate-tenant.sh — Migrate a tenant from the public platform to this private deployment.
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# 1. Connects to the source (public) MySQL and locates the tenant by email or username.
|
||||||
|
# 2. Exports t_tenant, t_app, and t_feature_service rows for that tenant.
|
||||||
|
# 3. Replaces the bootstrap tenant in the private deployment's MySQL with the migrated data.
|
||||||
|
# 4. Restarts tenant-service to flush any in-memory state.
|
||||||
|
# 5. Verifies the migration via the SDK config API.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/migrate-tenant.sh [options]
|
||||||
|
#
|
||||||
|
# Required options:
|
||||||
|
# --src-host HOST Source MySQL host (production server)
|
||||||
|
# --src-user USER Source MySQL username
|
||||||
|
# --src-password PASS Source MySQL password
|
||||||
|
# --tenant EMAIL|USERNAME Tenant to migrate (matched against email or username)
|
||||||
|
#
|
||||||
|
# Optional options:
|
||||||
|
# --src-port PORT Source MySQL port (default: 3306)
|
||||||
|
# --src-db DB Source database name (default: xuqm_tenant)
|
||||||
|
# --dry-run Print generated SQL but do not apply it
|
||||||
|
# --reset-password PASS Override tenant password after migration (bcrypt-hashed on server)
|
||||||
|
# -h, --help Show this help
|
||||||
|
#
|
||||||
|
# The source MySQL client is resolved in order: docker exec (managed container),
|
||||||
|
# then a local mysql binary. At least one must be available.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
. "$ROOT_DIR/scripts/lib.sh"
|
||||||
|
load_env
|
||||||
|
|
||||||
|
SRC_HOST=""
|
||||||
|
SRC_PORT="3306"
|
||||||
|
SRC_USER=""
|
||||||
|
SRC_PASSWORD=""
|
||||||
|
SRC_DB="xuqm_tenant"
|
||||||
|
TENANT_IDENT=""
|
||||||
|
DRY_RUN=false
|
||||||
|
RESET_PASSWORD=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
awk '/^# Usage:/,/^[^#]/' "$0" | grep '^#' | sed 's/^# \{0,1\}//'
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--src-host) SRC_HOST="${2:-}"; shift 2 ;;
|
||||||
|
--src-host=*) SRC_HOST="${1#--src-host=}"; shift ;;
|
||||||
|
--src-port) SRC_PORT="${2:-}"; shift 2 ;;
|
||||||
|
--src-port=*) SRC_PORT="${1#--src-port=}"; shift ;;
|
||||||
|
--src-user) SRC_USER="${2:-}"; shift 2 ;;
|
||||||
|
--src-user=*) SRC_USER="${1#--src-user=}"; shift ;;
|
||||||
|
--src-password) SRC_PASSWORD="${2:-}"; shift 2 ;;
|
||||||
|
--src-password=*) SRC_PASSWORD="${1#--src-password=}"; shift ;;
|
||||||
|
--src-db) SRC_DB="${2:-}"; shift 2 ;;
|
||||||
|
--src-db=*) SRC_DB="${1#--src-db=}"; shift ;;
|
||||||
|
--tenant) TENANT_IDENT="${2:-}"; shift 2 ;;
|
||||||
|
--tenant=*) TENANT_IDENT="${1#--tenant=}"; shift ;;
|
||||||
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
|
--reset-password) RESET_PASSWORD="${2:-}"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) fail_json "XUQM_PRIVATE_5001" "unknown option: $1" "migrate-tenant" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$SRC_HOST" ] || fail_json "XUQM_PRIVATE_5002" "--src-host is required" "migrate-tenant"
|
||||||
|
[ -n "$SRC_USER" ] || fail_json "XUQM_PRIVATE_5003" "--src-user is required" "migrate-tenant"
|
||||||
|
[ -n "$SRC_PASSWORD" ] || fail_json "XUQM_PRIVATE_5004" "--src-password is required" "migrate-tenant"
|
||||||
|
[ -n "$TENANT_IDENT" ] || fail_json "XUQM_PRIVATE_5005" "--tenant is required" "migrate-tenant"
|
||||||
|
|
||||||
|
audit "migrate-tenant" "STARTED" "src=$SRC_HOST tenant=$TENANT_IDENT dry_run=$DRY_RUN"
|
||||||
|
progress "migrate-tenant" "STARTED" "src=$SRC_HOST tenant=$TENANT_IDENT"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Resolve source and destination MySQL clients
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Source: always a remote host, so require a local mysql binary
|
||||||
|
require_cmd mysql
|
||||||
|
|
||||||
|
src_mysql() {
|
||||||
|
MYSQL_PWD="$SRC_PASSWORD" mysql \
|
||||||
|
-h "$SRC_HOST" -P "$SRC_PORT" -u "$SRC_USER" \
|
||||||
|
--default-character-set=utf8mb4 --connect-timeout=10 \
|
||||||
|
"$SRC_DB" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Destination: prefer managed container, fall back to local binary
|
||||||
|
DST_MYSQL_CTR="$(compose ps -q mysql 2>/dev/null | head -1 || true)"
|
||||||
|
|
||||||
|
dst_mysql() {
|
||||||
|
if [ -n "$DST_MYSQL_CTR" ]; then
|
||||||
|
docker exec -i "$DST_MYSQL_CTR" \
|
||||||
|
mysql -u "${MYSQL_USERNAME:-xuqm}" -p"${MYSQL_PASSWORD:-}" \
|
||||||
|
--default-character-set=utf8mb4 \
|
||||||
|
"${MYSQL_DATABASE:-xuqm_private}" "$@"
|
||||||
|
elif command -v mysql >/dev/null 2>&1; then
|
||||||
|
MYSQL_PWD="${MYSQL_PASSWORD:-}" mysql \
|
||||||
|
-h "${MYSQL_HOST:-127.0.0.1}" -P "${MYSQL_PORT:-3306}" \
|
||||||
|
-u "${MYSQL_USERNAME:-xuqm}" \
|
||||||
|
--default-character-set=utf8mb4 \
|
||||||
|
"${MYSQL_DATABASE:-xuqm_private}" "$@"
|
||||||
|
else
|
||||||
|
fail_json "XUQM_PRIVATE_5006" "no destination MySQL client: container not running and mysql binary not found" "migrate-tenant"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 1 — Locate the tenant in the source database
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
printf '\n[1/5] Locating tenant "%s" in %s/%s ...\n' "$TENANT_IDENT" "$SRC_HOST" "$SRC_DB"
|
||||||
|
|
||||||
|
TENANT_ROW="$(src_mysql -N -e "
|
||||||
|
SELECT id, username, nickname, email, status
|
||||||
|
FROM t_tenant
|
||||||
|
WHERE (email = '${TENANT_IDENT}' OR username = '${TENANT_IDENT}')
|
||||||
|
AND type = 'MAIN'
|
||||||
|
LIMIT 1" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
[ -n "$TENANT_ROW" ] || \
|
||||||
|
fail_json "XUQM_PRIVATE_5007" "tenant not found in source DB: $TENANT_IDENT" "migrate-tenant"
|
||||||
|
|
||||||
|
TENANT_ID="$(printf '%s' "$TENANT_ROW" | awk '{print $1}')"
|
||||||
|
TENANT_USERNAME="$(printf '%s' "$TENANT_ROW" | awk '{print $2}')"
|
||||||
|
TENANT_NICKNAME="$(printf '%s' "$TENANT_ROW" | awk '{print $3}')"
|
||||||
|
TENANT_EMAIL="$(printf '%s' "$TENANT_ROW" | awk '{print $4}')"
|
||||||
|
TENANT_STATUS="$(printf '%s' "$TENANT_ROW" | awk '{print $5}')"
|
||||||
|
|
||||||
|
printf ' Found: id=%s username=%s nickname=%s email=%s status=%s\n' \
|
||||||
|
"$TENANT_ID" "$TENANT_USERNAME" "$TENANT_NICKNAME" "$TENANT_EMAIL" "$TENANT_STATUS"
|
||||||
|
|
||||||
|
# Count associated data
|
||||||
|
APP_COUNT="$(src_mysql -N -e "SELECT COUNT(*) FROM t_app WHERE tenant_id='${TENANT_ID}'" 2>/dev/null || echo 0)"
|
||||||
|
FS_COUNT="$(src_mysql -N -e "
|
||||||
|
SELECT COUNT(*) FROM t_feature_service
|
||||||
|
WHERE app_key IN (SELECT app_key FROM t_app WHERE tenant_id='${TENANT_ID}')" 2>/dev/null || echo 0)"
|
||||||
|
|
||||||
|
printf ' Apps: %s FeatureServices: %s\n' "$APP_COUNT" "$FS_COUNT"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 2 — Collect app keys for this tenant
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
APP_KEYS_RAW="$(src_mysql -N -e "SELECT app_key FROM t_app WHERE tenant_id='${TENANT_ID}'" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [ -z "$APP_KEYS_RAW" ]; then
|
||||||
|
printf ' WARNING: tenant has no apps; only tenant record will be migrated.\n'
|
||||||
|
APP_KEYS_SQL="''"
|
||||||
|
else
|
||||||
|
# Build SQL IN-list: 'key1','key2',...
|
||||||
|
APP_KEYS_SQL="$(printf '%s' "$APP_KEYS_RAW" | awk '{printf "'"'"'%s'"'"',",$1}' | sed 's/,$//')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
FIRST_APP_KEY="$(printf '%s' "$APP_KEYS_RAW" | head -1 || true)"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 3 — Generate migration SQL
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
printf '\n[2/5] Generating migration SQL ...\n'
|
||||||
|
|
||||||
|
SQL_FILE="$(mktemp /tmp/xuqm-migrate-XXXXXX.sql)"
|
||||||
|
trap 'rm -f "$SQL_FILE"' EXIT
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '-- XuqmGroup private deployment tenant migration\n'
|
||||||
|
printf '-- Source: %s/%s Tenant: %s (%s)\n' "$SRC_HOST" "$SRC_DB" "$TENANT_NICKNAME" "$TENANT_ID"
|
||||||
|
printf '-- Generated: %s\n\n' "$(now)"
|
||||||
|
|
||||||
|
printf 'SET FOREIGN_KEY_CHECKS=0;\n\n'
|
||||||
|
|
||||||
|
# Clear existing data (private deployment is single-tenant)
|
||||||
|
printf '-- Clear existing tenant data\n'
|
||||||
|
printf 'DELETE FROM t_feature_service;\n'
|
||||||
|
printf 'DELETE FROM t_app;\n'
|
||||||
|
printf 'DELETE FROM t_tenant;\n\n'
|
||||||
|
|
||||||
|
# t_tenant — use explicit column names to be schema-order agnostic
|
||||||
|
printf '-- Tenant record\n'
|
||||||
|
src_mysql -N -e "
|
||||||
|
SELECT CONCAT(
|
||||||
|
'INSERT INTO t_tenant (id,created_at,email,nickname,parent_id,password,phone,status,type,username) VALUES (',
|
||||||
|
QUOTE(id),',',QUOTE(created_at),',',QUOTE(email),',',QUOTE(nickname),',',
|
||||||
|
IFNULL(QUOTE(parent_id),'NULL'),',',QUOTE(password),',',
|
||||||
|
IFNULL(QUOTE(phone),'NULL'),',',QUOTE(status),',',QUOTE(type),',',QUOTE(username),');'
|
||||||
|
) FROM t_tenant WHERE id='${TENANT_ID}'" 2>/dev/null
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
# t_app — explicit columns matching private schema (alphabetical: id,app_key,...,tenant_id)
|
||||||
|
if [ "$APP_COUNT" -gt 0 ]; then
|
||||||
|
printf '-- Apps\n'
|
||||||
|
src_mysql -N -e "
|
||||||
|
SELECT CONCAT(
|
||||||
|
'INSERT INTO t_app (id,app_key,app_secret,created_at,description,harmony_bundle_name,icon_url,ios_bundle_id,name,package_name,tenant_id) VALUES (',
|
||||||
|
QUOTE(id),',',QUOTE(app_key),',',QUOTE(app_secret),',',QUOTE(created_at),',',
|
||||||
|
IFNULL(QUOTE(description),'NULL'),',',IFNULL(QUOTE(harmony_bundle_name),'NULL'),',',
|
||||||
|
IFNULL(QUOTE(icon_url),'NULL'),',',IFNULL(QUOTE(ios_bundle_id),'NULL'),',',
|
||||||
|
QUOTE(name),',',QUOTE(package_name),',',QUOTE(tenant_id),');'
|
||||||
|
) FROM t_app WHERE tenant_id='${TENANT_ID}'" 2>/dev/null
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# t_feature_service — explicit columns matching private schema (alphabetical: id,app_key,config,...)
|
||||||
|
if [ "$FS_COUNT" -gt 0 ]; then
|
||||||
|
printf '-- Feature services\n'
|
||||||
|
src_mysql -N -e "
|
||||||
|
SELECT CONCAT(
|
||||||
|
'INSERT INTO t_feature_service (id,app_key,config,created_at,enabled,platform,secret_key,service_type) VALUES (',
|
||||||
|
QUOTE(id),',',QUOTE(app_key),',',IFNULL(QUOTE(config),'NULL'),',',QUOTE(created_at),',',
|
||||||
|
CAST(enabled AS UNSIGNED),',',QUOTE(platform),',',QUOTE(secret_key),',',QUOTE(service_type),');'
|
||||||
|
) FROM t_feature_service WHERE app_key IN (${APP_KEYS_SQL})" 2>/dev/null
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Optional password reset via bcrypt (requires htpasswd or python3 available on the deployment host)
|
||||||
|
if [ -n "$RESET_PASSWORD" ]; then
|
||||||
|
printf '-- Password reset\n'
|
||||||
|
if command -v python3 >/dev/null 2>&1 && python3 -c "import bcrypt" 2>/dev/null; then
|
||||||
|
BCRYPT_HASH="$(python3 -c "import bcrypt; print(bcrypt.hashpw(b'${RESET_PASSWORD}', bcrypt.gensalt(rounds=10)).decode())")"
|
||||||
|
elif command -v htpasswd >/dev/null 2>&1; then
|
||||||
|
BCRYPT_HASH="$(htpasswd -bnBC 10 '' "${RESET_PASSWORD}" | tr -d ':\n' | sed 's/\$2y/\$2a/')"
|
||||||
|
else
|
||||||
|
printf '-- WARNING: cannot generate bcrypt hash (install python3-bcrypt or apache2-utils)\n'
|
||||||
|
BCRYPT_HASH=""
|
||||||
|
fi
|
||||||
|
if [ -n "$BCRYPT_HASH" ]; then
|
||||||
|
printf "UPDATE t_tenant SET password='%s' WHERE id='%s';\n" "$BCRYPT_HASH" "$TENANT_ID"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'SET FOREIGN_KEY_CHECKS=1;\n'
|
||||||
|
printf '-- Migration SQL complete (%s apps, %s feature services)\n' "$APP_COUNT" "$FS_COUNT"
|
||||||
|
} > "$SQL_FILE"
|
||||||
|
|
||||||
|
LINE_COUNT="$(wc -l < "$SQL_FILE")"
|
||||||
|
printf ' Generated %s lines of SQL\n' "$LINE_COUNT"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 4 — Apply (or dry-run)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
|
printf '\n[DRY RUN] SQL that would be applied:\n'
|
||||||
|
printf '%s\n' "---"
|
||||||
|
cat "$SQL_FILE"
|
||||||
|
printf '%s\n' "---"
|
||||||
|
printf '\nDry run complete. Re-run without --dry-run to apply.\n'
|
||||||
|
audit "migrate-tenant" "DRY_RUN" "tenant=$TENANT_ID apps=$APP_COUNT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n[3/5] Applying migration to private deployment ...\n'
|
||||||
|
dst_mysql < "$SQL_FILE"
|
||||||
|
audit "migrate-tenant" "SQL_APPLIED" "tenant=$TENANT_ID apps=$APP_COUNT fs=$FS_COUNT"
|
||||||
|
printf ' Done.\n'
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 5 — Restart tenant-service to flush any cached state
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
printf '\n[4/5] Restarting tenant-service ...\n'
|
||||||
|
TENANT_SVC_CTR="$(compose ps -q tenant-service 2>/dev/null | head -1 || true)"
|
||||||
|
if [ -n "$TENANT_SVC_CTR" ]; then
|
||||||
|
compose restart tenant-service
|
||||||
|
# Wait up to 30 s for the service to come back
|
||||||
|
WAITED=0
|
||||||
|
while [ "$WAITED" -lt 30 ]; do
|
||||||
|
sleep 3
|
||||||
|
WAITED=$((WAITED + 3))
|
||||||
|
HTTP_CODE="$(curl -skL -o /dev/null -w '%{http_code}' --max-time 3 \
|
||||||
|
"http://localhost:${NGINX_PORT:-80}/actuator/health" 2>/dev/null || echo '000')"
|
||||||
|
[ "$HTTP_CODE" = "200" ] && break
|
||||||
|
done
|
||||||
|
[ "$HTTP_CODE" = "200" ] || printf ' WARNING: actuator/health not 200 after restart (HTTP %s); continuing.\n' "$HTTP_CODE"
|
||||||
|
printf ' tenant-service restarted.\n'
|
||||||
|
else
|
||||||
|
printf ' WARNING: tenant-service container not found; skipping restart.\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 6 — Verify
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
printf '\n[5/5] Verifying migration ...\n'
|
||||||
|
|
||||||
|
BASE_URL="http://localhost:${NGINX_PORT:-80}"
|
||||||
|
[ -n "${CONSOLE_DOMAIN:-}" ] && BASE_URL="${CONSOLE_DOMAIN}"
|
||||||
|
|
||||||
|
# Check tenant record
|
||||||
|
DST_TENANT="$(dst_mysql -N -e "
|
||||||
|
SELECT id, username, nickname, email FROM t_tenant WHERE id='${TENANT_ID}'" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [ -n "$DST_TENANT" ]; then
|
||||||
|
printf ' DB check: PASS tenant record present (%s)\n' "$TENANT_NICKNAME"
|
||||||
|
else
|
||||||
|
fail_json "XUQM_PRIVATE_5008" "tenant record not found in destination after migration" "migrate-tenant"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DST_APPS="$(dst_mysql -N -e "SELECT COUNT(*) FROM t_app WHERE tenant_id='${TENANT_ID}'" 2>/dev/null || echo 0)"
|
||||||
|
printf ' DB check: PASS %s app(s) migrated\n' "$DST_APPS"
|
||||||
|
|
||||||
|
# Check SDK config endpoint for the first migrated app
|
||||||
|
if [ -n "$FIRST_APP_KEY" ] && command -v curl >/dev/null 2>&1; then
|
||||||
|
SDK_CODE="$(curl -skL -o /dev/null -w '%{http_code}' --max-time 5 \
|
||||||
|
"${BASE_URL}/api/sdk/config?appKey=${FIRST_APP_KEY}&platform=ANDROID" 2>/dev/null || echo '000')"
|
||||||
|
if [ "$SDK_CODE" = "200" ]; then
|
||||||
|
printf ' API check: PASS /api/sdk/config returned 200 for %s\n' "$FIRST_APP_KEY"
|
||||||
|
else
|
||||||
|
printf ' API check: WARN /api/sdk/config returned %s for %s (service may still be warming up)\n' \
|
||||||
|
"$SDK_CODE" "$FIRST_APP_KEY"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Confirm deployment status
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
DEPLOY_STATUS="$(curl -skL --max-time 5 "${BASE_URL}/api/private/deployment/status" 2>/dev/null || true)"
|
||||||
|
if printf '%s' "$DEPLOY_STATUS" | grep -q '"mode":"PRIVATE"'; then
|
||||||
|
REG_ENABLED="$(printf '%s' "$DEPLOY_STATUS" | grep -o '"tenantRegisterEnabled":[^,}]*' | cut -d: -f2)"
|
||||||
|
printf ' API check: PASS PRIVATE mode active, tenantRegisterEnabled=%s\n' "$REG_ENABLED"
|
||||||
|
else
|
||||||
|
printf ' API check: WARN deployment/status did not confirm PRIVATE mode\n'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\nMigration complete.\n'
|
||||||
|
printf ' Tenant: %s (%s)\n' "$TENANT_NICKNAME" "$TENANT_EMAIL"
|
||||||
|
printf ' Apps: %s\n' "$DST_APPS"
|
||||||
|
[ -n "$RESET_PASSWORD" ] && printf ' Password: reset to provided value\n'
|
||||||
|
printf ' Login URL: %s\n' "${BASE_URL:-http://localhost}"
|
||||||
|
|
||||||
|
audit "migrate-tenant" "DONE" "tenant=$TENANT_ID apps=$DST_APPS"
|
||||||
|
progress "migrate-tenant" "DONE" "tenant=$TENANT_NICKNAME email=$TENANT_EMAIL apps=$DST_APPS"
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户