293 lines
8.8 KiB
Bash
293 lines
8.8 KiB
Bash
#!/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
|
||
# 兼容:JWT 也放在 prod-credentials
|
||
load_secret JWT_SECRET_KEY prod-credentials JWT_SECRET_KEY
|
||
|
||
: "${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)}"
|
||
|
||
# Consul:runner 可能在另一集群节点上,ClusterIP 不可达 → 一律 port-forward 到本机
|
||
CONSUL_PF_PID=""
|
||
cleanup() {
|
||
if [[ -n "${CONSUL_PF_PID:-}" ]]; then
|
||
kill "$CONSUL_PF_PID" 2>/dev/null || true
|
||
wait "$CONSUL_PF_PID" 2>/dev/null || true
|
||
CONSUL_PF_PID=""
|
||
fi
|
||
[[ -n "${TMP:-}" && -d "${TMP:-}" ]] && rm -rf "$TMP"
|
||
}
|
||
trap cleanup EXIT
|
||
|
||
if [[ -z "$CONSUL_HTTP_ADDR" ]]; then
|
||
if ! kubectl -n "$NS" get svc consul >/dev/null 2>&1; then
|
||
echo "FAIL: 无 svc/${NS}/consul(KUBECONFIG=${KUBECONFIG})"
|
||
exit 1
|
||
fi
|
||
# 避开常用端口;失败则换一个
|
||
LOCAL_PORT=""
|
||
for p in 18500 18501 18502 18503 18504; do
|
||
if ! ss -ltn 2>/dev/null | grep -q ":${p} " && ! netstat -ltn 2>/dev/null | grep -q ":${p} "; then
|
||
LOCAL_PORT=$p
|
||
break
|
||
fi
|
||
done
|
||
LOCAL_PORT="${LOCAL_PORT:-18500}"
|
||
echo "==> port-forward svc/consul → 127.0.0.1:${LOCAL_PORT} (ns=${NS})"
|
||
kubectl -n "$NS" port-forward svc/consul "${LOCAL_PORT}:8500" >/tmp/consul-pf-${NS}.log 2>&1 &
|
||
CONSUL_PF_PID=$!
|
||
ok=0
|
||
for _ in $(seq 1 40); do
|
||
if ! kill -0 "$CONSUL_PF_PID" 2>/dev/null; then
|
||
echo "FAIL: port-forward 已退出,见 /tmp/consul-pf-${NS}.log"
|
||
cat /tmp/consul-pf-${NS}.log 2>/dev/null || true
|
||
exit 1
|
||
fi
|
||
if curl -sf --connect-timeout 1 "http://127.0.0.1:${LOCAL_PORT}/v1/status/leader" >/dev/null 2>&1; then
|
||
ok=1
|
||
break
|
||
fi
|
||
sleep 0.5
|
||
done
|
||
if [[ "$ok" != "1" ]]; then
|
||
echo "FAIL: Consul port-forward 未就绪,见 /tmp/consul-pf-${NS}.log"
|
||
cat /tmp/consul-pf-${NS}.log 2>/dev/null || true
|
||
exit 1
|
||
fi
|
||
CONSUL_HTTP_ADDR="http://127.0.0.1:${LOCAL_PORT}"
|
||
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)"
|
||
|
||
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
|
||
envsubst '${MYSQL_ROOT_PASSWORD} ${REDIS_PASSWORD} ${RABBITMQ_DEFAULT_PASS} ${ELASTIC_PASSWORD} ${JWT_SECRET_KEY}' \
|
||
<"$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
|
||
|
||
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
|
||
|
||
echo "DONE"
|