#!/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)" 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/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)}" if [[ -z "$CONSUL_HTTP_ADDR" ]]; then CIP="$(kubectl -n "$NS" get svc consul -o jsonpath='{.spec.clusterIP}')" CONSUL_HTTP_ADDR="http://${CIP}:8500" 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 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"