config/scripts/sync-to-consul.sh
xiaolipro f156204e1e
All checks were successful
Sync Consul Config / sync (push) Successful in 3s
111
2026-09-21 17:52:21 +08:00

296 lines
10 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
# 将仓库 idwms/*.json 渲染密钥后写入 Consul KV
# 用法:
# ./scripts/sync-to-consul.sh # 同步全部
# ./scripts/sync-to-consul.sh admin # 只同步 idwms/admin
# RESTART_APIS=1 ./scripts/sync-to-consul.sh
#
# 密钥来源(优先):
# 1) 环境变量 MYSQL_ROOT_PASSWORD / REDIS_PASSWORD / ...
# 2) K8s Secret prod-credentials(及可选 jwt-secret)
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
export KUBECONFIG="${KUBECONFIG:-/etc/rancher/k3s/k3s.yaml}"
NS="${NAMESPACE:-prod}"
CONSUL_HTTP_ADDR="${CONSUL_HTTP_ADDR:-}"
RESTART_APIS="${RESTART_APIS:-0}"
IDWMS_DIR="${ROOT}/idwms"
need() { command -v "$1" >/dev/null || { echo "FAIL: 需要 $1"; exit 1; }; }
need kubectl
need curl
need python3
command -v envsubst >/dev/null || { echo "FAIL: 需要 envsubst(apt install gettext-base)"; exit 1; }
# 按 JSONC 校验(允许 //、/* */、尾逗号);通过后原样写入 Consul,不剥离注释
jsonc_check() {
python3 - "$1" <<'PY'
import json, sys
def strip_jsonc(text: str) -> str:
out = []
i, n = 0, len(text)
in_str = escape = False
while i < n:
c = text[i]
if in_str:
out.append(c)
if escape:
escape = False
elif c == "\\":
escape = True
elif c == '"':
in_str = False
i += 1
continue
if c == '"':
in_str = True
out.append(c)
i += 1
continue
if c == "/" and i + 1 < n:
nxt = text[i + 1]
if nxt == "/":
i += 2
while i < n and text[i] not in "\r\n":
i += 1
continue
if nxt == "*":
i += 2
while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
i += 1
i = min(i + 2, n)
continue
out.append(c)
i += 1
cleaned = []
s = "".join(out)
i, n = 0, len(s)
in_str = escape = False
while i < n:
c = s[i]
if in_str:
cleaned.append(c)
if escape:
escape = False
elif c == "\\":
escape = True
elif c == '"':
in_str = False
i += 1
continue
if c == '"':
in_str = True
cleaned.append(c)
i += 1
continue
if c == ",":
j = i + 1
while j < n and s[j] in " \t\r\n":
j += 1
if j < n and s[j] in "]}":
i += 1
continue
cleaned.append(c)
i += 1
return "".join(cleaned)
path = sys.argv[1]
raw = open(path, encoding="utf-8").read()
try:
json.loads(strip_jsonc(raw))
except json.JSONDecodeError as e:
print(f"FAIL: {path} 不是合法 JSONC: {e}", file=sys.stderr)
sys.exit(1)
PY
}
secret_b64() {
local name="$1" key="$2"
kubectl -n "$NS" get secret "$name" -o "jsonpath={.data.${key}}" 2>/dev/null || true
}
load_secret() {
local var="$1" secret="$2" key="$3"
if [[ -n "${!var:-}" ]]; then
return 0
fi
local raw
raw="$(secret_b64 "$secret" "$key")"
if [[ -n "$raw" ]]; then
export "$var"="$(printf '%s' "$raw" | base64 -d)"
fi
}
echo "==> 加载密钥(K8s Secret / env) ns=${NS} kubeconfig=${KUBECONFIG}"
if ! kubectl -n "$NS" get secret prod-credentials >/dev/null 2>&1; then
echo "FAIL: 找不到 secret/${NS}/prod-credentials"
echo " 当前: KUBECONFIG=${KUBECONFIG}"
echo " test 集群请先: kubectl --kubeconfig=/etc/rancher/k3s/k3s-test.yaml apply -f /data/k3s-test/manifests/secrets.yaml"
echo " 或: kubectl -n ${NS} get secret"
exit 1
fi
load_secret MYSQL_ROOT_PASSWORD prod-credentials MYSQL_ROOT_PASSWORD
load_secret REDIS_PASSWORD prod-credentials REDIS_PASSWORD
load_secret RABBITMQ_DEFAULT_PASS prod-credentials RABBITMQ_DEFAULT_PASS
load_secret ELASTIC_PASSWORD prod-credentials ELASTIC_PASSWORD
load_secret JWT_SECRET_KEY jwt-secret JWT_SECRET_KEY
load_secret JWT_SECRET_KEY prod-credentials JWT_SECRET_KEY
load_secret COS_SECRET_ID prod-credentials COS_SECRET_ID
load_secret COS_SECRET_KEY prod-credentials COS_SECRET_KEY
load_secret FEISHU_WEBHOOK prod-credentials FEISHU_WEBHOOK
load_secret WECHAT_MP_APP_ID prod-credentials WECHAT_MP_APP_ID
load_secret WECHAT_MP_SECRET prod-credentials WECHAT_MP_SECRET
load_secret WECHAT_MP_TOKEN prod-credentials WECHAT_MP_TOKEN
load_secret OMS_TRACK_APP_KEY prod-credentials OMS_TRACK_APP_KEY
load_secret OMS_TRACK_APP_SECRET prod-credentials OMS_TRACK_APP_SECRET
load_secret LAZADA_APP_KEY prod-credentials LAZADA_APP_KEY
load_secret LAZADA_APP_SECRET prod-credentials LAZADA_APP_SECRET
load_secret SHOPEE_PARTNER_ID prod-credentials SHOPEE_PARTNER_ID
load_secret SHOPEE_PARTNER_KEY prod-credentials SHOPEE_PARTNER_KEY
load_secret TIKTOK_APP_KEY prod-credentials TIKTOK_APP_KEY
load_secret TIKTOK_APP_SECRET prod-credentials TIKTOK_APP_SECRET
load_secret TEMU_APP_KEY prod-credentials TEMU_APP_KEY
load_secret TEMU_APP_SECRET prod-credentials TEMU_APP_SECRET
: "${MYSQL_ROOT_PASSWORD:?缺少 MYSQL_ROOT_PASSWORD(env 或 secret/${NS}/prod-credentials)}"
: "${REDIS_PASSWORD:?缺少 REDIS_PASSWORD}"
: "${RABBITMQ_DEFAULT_PASS:?缺少 RABBITMQ_DEFAULT_PASS}"
: "${ELASTIC_PASSWORD:?缺少 ELASTIC_PASSWORD}"
: "${JWT_SECRET_KEY:?缺少 JWT_SECRET_KEY(请创建 secret/jwt-secret 或写入 prod-credentials)}"
: "${COS_SECRET_ID:?缺少 COS_SECRET_ID}"
: "${COS_SECRET_KEY:?缺少 COS_SECRET_KEY}"
: "${FEISHU_WEBHOOK:?缺少 FEISHU_WEBHOOK}"
: "${WECHAT_MP_APP_ID:?缺少 WECHAT_MP_APP_ID}"
: "${WECHAT_MP_SECRET:?缺少 WECHAT_MP_SECRET}"
: "${WECHAT_MP_TOKEN:?缺少 WECHAT_MP_TOKEN}"
: "${OMS_TRACK_APP_KEY:?缺少 OMS_TRACK_APP_KEY}"
: "${OMS_TRACK_APP_SECRET:?缺少 OMS_TRACK_APP_SECRET}"
: "${LAZADA_APP_KEY:?缺少 LAZADA_APP_KEY}"
: "${LAZADA_APP_SECRET:?缺少 LAZADA_APP_SECRET}"
: "${SHOPEE_PARTNER_ID:?缺少 SHOPEE_PARTNER_ID}"
: "${SHOPEE_PARTNER_KEY:?缺少 SHOPEE_PARTNER_KEY}"
: "${TIKTOK_APP_KEY:?缺少 TIKTOK_APP_KEY}"
: "${TIKTOK_APP_SECRET:?缺少 TIKTOK_APP_SECRET}"
: "${TEMU_APP_KEY:?缺少 TEMU_APP_KEY}"
: "${TEMU_APP_SECRET:?缺少 TEMU_APP_SECRET}"
# Consul:prod 用 ClusterIP(runner 在簇内);test 用内网 NodePort(见 consul-ci / TEST_CONSUL_HTTP_ADDR)
if [[ -z "$CONSUL_HTTP_ADDR" ]]; then
if [[ "$NS" == "test" ]]; then
# 安全组:仅放行 CI 机内网 → 测试机 30850;勿对公网开放
CONSUL_HTTP_ADDR="${TEST_CONSUL_HTTP_ADDR:-http://172.16.16.13:30850}"
else
CIP="$(kubectl -n "$NS" get svc consul -o jsonpath='{.spec.clusterIP}')"
CONSUL_HTTP_ADDR="http://${CIP}:8500"
fi
fi
echo "==> Consul ${CONSUL_HTTP_ADDR}"
# ACL(可选)
if [[ -z "${CONSUL_HTTP_TOKEN:-}" ]]; then
raw="$(secret_b64 consul-acl CONSUL_HTTP_TOKEN)"
if [[ -n "$raw" ]]; then
export CONSUL_HTTP_TOKEN="$(printf '%s' "$raw" | base64 -d)"
fi
fi
CURL_AUTH=()
if [[ -n "${CONSUL_HTTP_TOKEN:-}" ]]; then
CURL_AUTH=(-H "X-Consul-Token: ${CONSUL_HTTP_TOKEN}")
fi
TARGETS=()
if [[ $# -eq 0 ]]; then
for f in "${IDWMS_DIR}"/*.json; do
[[ -f "$f" ]] || continue
TARGETS+=("$(basename "$f" .json)")
done
else
TARGETS=("$@")
fi
[[ ${#TARGETS[@]} -gt 0 ]] || { echo "FAIL: 无同步目标"; exit 1; }
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
put_kv() {
local key="$1" file="$2"
local code
code="$(curl -sS -o /tmp/consul-put.out -w '%{http_code}' \
"${CURL_AUTH[@]}" \
-X PUT --data-binary @"$file" \
"${CONSUL_HTTP_ADDR}/v1/kv/${key}")"
if [[ "$code" != "200" ]]; then
echo "FAIL: PUT ${key} HTTP ${code}"
cat /tmp/consul-put.out 2>/dev/null || true
exit 1
fi
echo "OK idwms/${key##*/} → ${key}"
}
RESTART_SET=()
for name in "${TARGETS[@]}"; do
src="${IDWMS_DIR}/${name}.json"
[[ -f "$src" ]] || { echo "FAIL: 无 ${src}"; exit 1; }
# 仅替换已知占位符,避免 JSON 里其它 $ 被误伤
export MYSQL_ROOT_PASSWORD REDIS_PASSWORD RABBITMQ_DEFAULT_PASS ELASTIC_PASSWORD JWT_SECRET_KEY \
COS_SECRET_ID COS_SECRET_KEY FEISHU_WEBHOOK \
WECHAT_MP_APP_ID WECHAT_MP_SECRET WECHAT_MP_TOKEN \
OMS_TRACK_APP_KEY OMS_TRACK_APP_SECRET \
LAZADA_APP_KEY LAZADA_APP_SECRET \
SHOPEE_PARTNER_ID SHOPEE_PARTNER_KEY \
TIKTOK_APP_KEY TIKTOK_APP_SECRET \
TEMU_APP_KEY TEMU_APP_SECRET
envsubst '${MYSQL_ROOT_PASSWORD} ${REDIS_PASSWORD} ${RABBITMQ_DEFAULT_PASS} ${ELASTIC_PASSWORD} ${JWT_SECRET_KEY} ${COS_SECRET_ID} ${COS_SECRET_KEY} ${FEISHU_WEBHOOK} ${WECHAT_MP_APP_ID} ${WECHAT_MP_SECRET} ${WECHAT_MP_TOKEN} ${OMS_TRACK_APP_KEY} ${OMS_TRACK_APP_SECRET} ${LAZADA_APP_KEY} ${LAZADA_APP_SECRET} ${SHOPEE_PARTNER_ID} ${SHOPEE_PARTNER_KEY} ${TIKTOK_APP_KEY} ${TIKTOK_APP_SECRET} ${TEMU_APP_KEY} ${TEMU_APP_SECRET}' \
<"$src" >"${TMP}/${name}.json"
# 禁止未替换的占位符漏网(注释里的 ${...} 也会拦)
if grep -qE '\$\{[A-Z0-9_]+\}' "${TMP}/${name}.json"; then
echo "FAIL: ${name}.json 仍有未替换占位符:"
grep -E '\$\{[A-Z0-9_]+\}' "${TMP}/${name}.json" || true
exit 1
fi
# JSONC 校验通过后,原样 PUT(保留注释,便于和 git 对照排查)
if ! jsonc_check "${TMP}/${name}.json"; then
exit 1
fi
put_kv "idwms/${name}" "${TMP}/${name}.json"
case "$name" in
admin) RESTART_SET+=(admin-api) ;;
itfxapi) RESTART_SET+=(interfaceforward-api) ;;
trackapi) RESTART_SET+=(track-api) ;;
common)
RESTART_SET+=(admin-api interfaceforward-api track-api)
;;
esac
done
# 暂时关闭:改配置后自动 rollout restart API(需要时把下面取消注释,或 CI 设 RESTART_APIS=1)
# if [[ "$RESTART_APIS" == "1" ]]; then
# readarray -t RESTART_SET < <(printf '%s\n' "${RESTART_SET[@]}" | sort -u)
# echo "==> rollout restart: ${RESTART_SET[*]}"
# for d in "${RESTART_SET[@]}"; do
# [[ -n "$d" ]] || continue
# if kubectl -n "$NS" get "deploy/${d}" >/dev/null 2>&1; then
# kubectl -n "$NS" rollout restart "deploy/${d}"
# kubectl -n "$NS" rollout status "deploy/${d}" --timeout=180s || true
# else
# echo "SKIP deploy/${d}(不存在)"
# fi
# done
# fi
if [[ "$RESTART_APIS" == "1" ]]; then
echo "==> SKIP rollout restart(已暂时关闭;RESTART_APIS=1 被忽略)"
fi
echo "DONE"