242 lines
7.0 KiB
Bash
242 lines
7.0 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)"
|
||
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"
|