刘光辉
11 小时以前 34981c30a78e8bbd7791131059a9210f9928b62c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# 开一个客户的 PG 实例(docs/adr-multi-customer-db-isolation.md §8)。
#
# 只是把四个【已有且都支持 CONTAINER 参数】的脚本串起来,本身不含任何建库逻辑:
#   compose up → install.sh(建四库+schema)→ create-app-roles.sh(角色+配额)
#   → harden-pg-hba.sh(postgres 超户禁公网)
# 幂等:容器已在跑会跳过重建;install.sh 对非空库会自己拒绝(除非 FORCE_RESEED=1)。
#
# 用法(在 /home/ubuntu/jnpf-pg/ 或仓库根)。客户 ID 直接用【客户拼音】(2026-08-08 命名规范:
# 容器名 cx-pg-<拼音>,一眼可辨客户;c2 是客户B的历史 ID,其容器名靠 CUSTOMER_NAME 对齐拼音):
#   ./postgres/tencent/add-customer.sh customer22 15434
#   ./postgres/tencent/add-customer.sh tpl     15439
#
# 凭据文件:每个客户【一份自己的】,用 ENV_FILE 指定(强烈建议,见下):
#   ENV_FILE=/home/ubuntu/jnpf-pg/.env.某客户 ./postgres/tencent/add-customer.sh customer22 15434
# 不指定则回退到共享的 .env。
#
# ⚠️ 为什么要每客户一份:密码与连接配额是【按实例】不同的,而 create-app-roles.sh 会按凭据
#    文件【重置】角色密码。共用一份 .env 就只能"临时改 → 跑 → 记得改回来",漏改一次就会在
#    下次对别的实例跑本脚本时,把那个客户的密码改成这个客户的。ENV_FILE 让这类事故不可能发生。
#
# 该文件需要的键(照 .env.example 抄,密码换成本客户独有的):
#   JNPF_PG_PASSWORD                                   超户密码
#   JNPF_APP_DB_PASSWORD / JNPF_FLOW_DB_PASSWORD / JNPF_NACOS_DB_PASSWORD
#   JNPF_APP_CONN_LIMIT=120 / JNPF_FLOW_CONN_LIMIT=20 / JNPF_NACOS_CONN_LIMIT=5
#     (对应本实例 max_connections=150,和 145 < 147)
#
# 另需:要从外网连时,云安全组已放行该端口(限来源 IP,别开 0.0.0.0/0)。
#
# 本脚本【不做】灌产品基线——那要人判断洗哪些数据,见 ADR §8 第 6 步。
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
 
CUSTOMER_ID="${1:-}"
CUSTOMER_PG_PORT="${2:-}"
[ -n "$CUSTOMER_ID" ] && [ -n "$CUSTOMER_PG_PORT" ] || {
  echo "用法:add-customer.sh <客户ID,如 c2/c3/tpl> <端口,客户N=15431+N,母版=15439>" >&2
  exit 1
}
 
log() { printf '\033[1;34m[add-customer]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[add-customer][err]\033[0m %s\n' "$*" >&2; exit 1; }
 
case "$CUSTOMER_ID" in
  ''|*[!a-z0-9]*) die "客户ID 只允许小写字母与数字(当前 '$CUSTOMER_ID'),它会成为容器名与卷名的一部分";;
esac
case "$CUSTOMER_PG_PORT" in
  ''|*[!0-9]*) die "端口必须是整数(当前 '$CUSTOMER_PG_PORT')";;
esac
 
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PG_DIR="$(dirname "$SCRIPT_DIR")"          # .../postgres
CONTAINER="cx-pg-${CUSTOMER_ID}"
 
# 护栏:绝不允许本脚本碰客户1 的既有实例(cx-postgres:15432,编排/内存/连接数都不同,
# 且 create-app-roles.sh 会按当前 .env 重置角色密码——跑错实例就改错了客户1 的密码)。
# 判据用【端口】而不是容器名:容器名恒为 cx-pg-<id>,永远不可能等于 cx-postgres,那种检查是死代码。
[ "$CUSTOMER_PG_PORT" = "15432" ] && die "15432 是客户1 既有实例 cx-postgres 的端口,本脚本不得操作它(新客户用 15431+N,母版用 15439)"
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qx "$CONTAINER"; then
  log "容器 ${CONTAINER} 已存在——按幂等继续(compose up 不会重建配置未变的容器)"
fi
 
for f in "$SCRIPT_DIR/docker-compose.customer-pg.yml" \
         "$PG_DIR/migration/install.sh" \
         "$PG_DIR/migration/create-app-roles.sh" \
         "$SCRIPT_DIR/harden-pg-hba.sh"; do
  [ -f "$f" ] || die "缺 $f(先把仓库的 docker/postgres/ 整目录同步过来,见 ADR §8)"
done
 
# 凭据文件:ENV_FILE 优先,否则用共享 .env(compose 与建角色脚本必须读同一份,
# 否则超户密码与业务角色密码会来自两个来源)
if [ -n "${ENV_FILE:-}" ]; then
  [ -f "$ENV_FILE" ] || die "ENV_FILE 指定的 $ENV_FILE 不存在"
  ENV_PATH="$(cd "$(dirname "$ENV_FILE")" && pwd)/$(basename "$ENV_FILE")"
else
  ENV_PATH=""; d="$SCRIPT_DIR"
  while [ "$d" != "/" ]; do
    [ -f "$d/.env" ] && { ENV_PATH="$d/.env"; break; }
    d="$(dirname "$d")"
  done
  [ -n "$ENV_PATH" ] || die "向上未找到 .env,且未指定 ENV_FILE"
  log "未指定 ENV_FILE,回退共享凭据文件 $ENV_PATH —— 多客户场景建议每客户一份,见文件头说明"
fi
log "凭据文件:$ENV_PATH"
 
log "1/4 起容器 ${CONTAINER}(端口 ${CUSTOMER_PG_PORT},max_connections=150,shared_buffers=128MB)"
CUSTOMER_ID="$CUSTOMER_ID" CUSTOMER_PG_PORT="$CUSTOMER_PG_PORT" \
  docker compose --env-file "$ENV_PATH" \
    -f "$SCRIPT_DIR/docker-compose.customer-pg.yml" up -d
 
log "2/4 建四库 + schema(install.sh,库名与客户1 完全一致)"
# 用 bash 显式调用,不依赖权限位——rsync/tar/zip 传播过程中执行位很容易丢
CONTAINER="$CONTAINER" bash "$PG_DIR/migration/install.sh"
 
log "3/4 建角色 + 连接配额(create-app-roles.sh,读 ${ENV_PATH} 的密码与 *_CONN_LIMIT)"
CONTAINER="$CONTAINER" ENV_FILE="$ENV_PATH" bash "$PG_DIR/migration/create-app-roles.sh"
 
log "4/4 收紧 pg_hba(postgres 超户禁止公网登录)"
CONTAINER="$CONTAINER" bash "$SCRIPT_DIR/harden-pg-hba.sh"
 
cat <<EOF
 
$(printf '\033[1;32m✓\033[0m') ${CONTAINER} 就绪。坐标 = <CVM公网IP>:${CUSTOMER_PG_PORT}
 
下一步:
  1. 灌产品基线:起 cx-pg-template → DATABASES="jnpf_init jnpf_flow" promote-db.sh dump/restore
     (务必显式限定这两个库,默认值含 nacos_config,那是环境私有的,见 ADR §3.5)
  2. 该客户各环境的 .env 填:
       CUSTOMER_DB_HOST=<CVM公网IP>
       CUSTOMER_DB_PORT=${CUSTOMER_PG_PORT}
  3. 本次用的凭据文件:${ENV_PATH}
$(if [ -z "${ENV_FILE:-}" ]; then printf '     \033[1;33m⚠\033[0m  用的是共享 .env——若刚才为本客户改过密码/配额,现在必须还原成客户1 的值,\n        否则下次对 cx-postgres 跑建角色脚本会改错密码。改用 ENV_FILE=<每客户一份> 可根除这个风险。'; else printf '     \033[1;32m✓\033[0m  用的是该客户专属凭据文件,未触碰其他客户的 .env'; fi)
EOF