#!/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"