checkpoint before checking out master

This commit is contained in:
xiaolipro 2026-09-07 17:35:47 +08:00
commit 276b8286d1
9 changed files with 489 additions and 0 deletions

View File

@ -0,0 +1,67 @@
# push 到 main/master → 渲染密钥 → 写入 Consul → 重启相关 API
# runner: k3s-host(与 api 部署同一台,可访问 ClusterIP + kubectl)
name: Sync Consul Config
on:
push:
branches: [master, main]
paths:
- "idwms/**"
- "scripts/**"
- ".gitea/workflows/sync-consul.yml"
workflow_dispatch:
inputs:
targets:
description: "同步目标(空=全部;或 admin / common / itfxapi / trackapi,空格分隔)"
required: false
default: ""
restart:
description: "同步后重启 API(1/0)"
required: false
default: "1"
jobs:
sync:
runs-on: k3s-host
steps:
- name: Checkout and sync
run: |
set -euxo pipefail
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
export NAMESPACE=prod
export RESTART_APIS="${{ gitea.event.inputs.restart || '1' }}"
if [[ "${{ gitea.event_name }}" == "push" ]]; then
export RESTART_APIS=1
fi
REPO="${{ gitea.repository }}"
REPO="${REPO:-${{ github.repository }}}"
SHA="${{ gitea.sha }}"
SHA="${SHA:-${{ github.sha }}}"
test -n "$REPO" && [[ "$REPO" == */* ]]
test -n "$SHA"
GITEA_HTTP="https://git.ideship.com"
WORK="/data/ci-build/config"
GITEA_USER="$(kubectl -n prod get secret kaniko-git -o jsonpath='{.data.GITEA_USER}' | base64 -d)"
GIT_TOKEN="$(kubectl -n prod get secret kaniko-git -o jsonpath='{.data.GIT_TOKEN}' | base64 -d)"
GITEA_USER="${GITEA_USER:-gitea}"
rm -rf "$WORK"
mkdir -p "$WORK"
cd "$WORK"
git init -q
git remote add origin "${GITEA_HTTP}/${REPO}.git"
AUTH_B64="$(printf '%s:%s' "$GITEA_USER" "$GIT_TOKEN" | base64 | tr -d '\n\r ')"
git -c http.extraHeader="Authorization: Basic ${AUTH_B64}" \
fetch --depth 1 --quiet --no-tags origin "$SHA"
git checkout --force --quiet FETCH_HEAD
chmod +x scripts/sync-to-consul.sh
TARGETS="${{ gitea.event.inputs.targets }}"
if [[ -n "${TARGETS// /}" ]]; then
# shellcheck disable=SC2086
./scripts/sync-to-consul.sh $TARGETS
else
./scripts/sync-to-consul.sh
fi

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.env
*.local.json
/tmp/
.DS_Store
Thumbs.db

92
README.md Normal file
View File

@ -0,0 +1,92 @@
# config — Consul 配置 GitOps
Gitea 仓保存 `idwms/*.json` → CI 用 K8s Secret 渲染密码 → `PUT` 到 Consul KV → 可选重启 API。
应用仍只读 Consul(`AddConsul("idwms/...")`),不读本仓库。
## 目录
| 文件 | Consul KV |
|------|-----------|
| `idwms/common.json` | `idwms/common` |
| `idwms/admin.json` | `idwms/admin` |
| `idwms/itfxapi.json` | `idwms/itfxapi` |
| `idwms/trackapi.json` | `idwms/trackapi` |
JSON 里敏感字段用占位符,例如 `${MYSQL_ROOT_PASSWORD}`。同步时从 `prod-credentials` / `jwt-secret` 注入,**明文密码不进 git**。
## 一次性准备
### 1. JWT(业务 API 用,中间件 Secret 里没有)
在 k3s 节点:
```bash
kubectl -n prod create secret generic jwt-secret \
--from-literal=JWT_SECRET_KEY='请换成足够长的随机串' \
--dry-run=client -o yaml | kubectl apply -f -
```
也可把 `JWT_SECRET_KEY` 加进现有 `manifests/secrets.yaml` 的 `prod-credentials`。
### 2. (可选)Consul ACL Token
若已开 ACL:
```bash
kubectl -n prod create secret generic consul-acl \
--from-literal=CONSUL_HTTP_TOKEN='...' \
--dry-run=client -o yaml | kubectl apply -f -
```
### 3. 推到 Gitea
```bash
cd d:/my/itfx/config
git remote add origin https://git.ideship.com/<org>/config.git
git add -A && git commit -m "chore: initial consul config repo"
git push -u origin master
```
在 Gitea 启用 Actions,确保 runner 标签含 `k3s-host`(与 API 部署相同)。
### 4. 与线上对账(推荐先做)
在能 `kubectl` 的机器上:
```bash
./scripts/export-from-consul.sh
# 把导出文件里的密码改回 ${MYSQL_ROOT_PASSWORD} 等占位符后再提交
```
避免模板库名/字段和现网不一致,第一次同步把线上冲掉。
## 日常
改 JSON → push `main`/`master` → workflow `Sync Consul Config` 自动同步并 `rollout restart` 对应 API。
手动:
```bash
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
./scripts/sync-to-consul.sh # 全部
RESTART_APIS=1 ./scripts/sync-to-consul.sh admin
```
## 密钥来源
| 占位符 | Secret |
|--------|--------|
| `${MYSQL_ROOT_PASSWORD}` | `prod-credentials` |
| `${REDIS_PASSWORD}` | `prod-credentials` |
| `${RABBITMQ_DEFAULT_PASS}` | `prod-credentials` |
| `${ELASTIC_PASSWORD}` | `prod-credentials` |
| `${JWT_SECRET_KEY}` | `jwt-secret` 或 `prod-credentials` |
与 `k3s/manifests/secrets.yaml` 一致:中间件用 Secret;本仓只存带占位符的业务配置模板。
## 注意
- `App:CorsOrigins` 在启动时打进 CORS 策略,改完必须重启 API(CI 默认会 restart)。
- 首次用仓库模板前务必 `export-from-consul` 对过现网,或确认 MySQL 库名(`oms` / `itfx` / `track`)正确。
- Consul 保持 ClusterIP,CI 经节点访问 Service ClusterIP,不暴露公网。

38
idwms/admin.json Normal file
View File

@ -0,0 +1,38 @@
{
"App": {
"Urls": ["http://+:5281"],
"CorsOrigins": [
"https://ideship.com",
"http://localhost:5173"
],
"SendStartupNotification": false
},
"ConnectionStrings": {
"Default": "Server=mysql;Port=3306;Database=oms;Uid=root;Pwd=${MYSQL_ROOT_PASSWORD};CharSet=utf8mb4;"
},
"Jwt": {
"SecretKey": "${JWT_SECRET_KEY}",
"Issuer": "SimpleAdmin.Api",
"Audience": "SimpleAdmin.Api",
"ExpiryInMinutes": "480",
"RefreshExpiryInDays": "7",
"ClockSkewMinutes": "5"
},
"JwtSettings": {
"SecretKey": "${JWT_SECRET_KEY}",
"Issuer": "SimpleAdmin.Api",
"Audience": "SimpleAdmin.Api",
"ExpiryInMinutes": "480",
"RefreshExpiryInDays": "7",
"ClockSkewMinutes": "5"
},
"FeiShuNotice": {
"Webhook": "",
"EnableFeiShuExceptionSubscribe": false
},
"ElasticSearch": {
"Uri": "http://elasticsearch:9200",
"Username": "elastic",
"Password": "${ELASTIC_PASSWORD}"
}
}

30
idwms/common.json Normal file
View File

@ -0,0 +1,30 @@
{
"Redis": {
"Host": "redis:6379",
"Password": "${REDIS_PASSWORD}",
"Database": 0,
"MaxPoolSize": 50,
"MinPoolSize": 1,
"IdleTimeout": 20000,
"ConnectTimeout": 5000
},
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "rabbitmq",
"Port": 5672,
"UserName": "oms",
"Password": "${RABBITMQ_DEFAULT_PASS}"
}
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning"
}
}
}
}

20
idwms/itfxapi.json Normal file
View File

@ -0,0 +1,20 @@
{
"App": {
"Urls": ["http://+:8888"],
"CorsOrigins": [
"https://ideship.com",
"http://localhost:5173"
]
},
"ConnectionStrings": {
"Default": "Server=mysql;Port=3306;Database=itfx;Uid=root;Pwd=${MYSQL_ROOT_PASSWORD};CharSet=utf8mb4;"
},
"Jwt": {
"SecretKey": "${JWT_SECRET_KEY}",
"Issuer": "InterfaceForward.Api",
"Audience": "InterfaceForward.Api",
"ExpiryInMinutes": "480",
"RefreshExpiryInDays": "7",
"ClockSkewMinutes": "5"
}
}

20
idwms/trackapi.json Normal file
View File

@ -0,0 +1,20 @@
{
"App": {
"Urls": ["http://+:8889"],
"CorsOrigins": [
"https://ideship.com",
"http://localhost:5173"
]
},
"ConnectionStrings": {
"Default": "Server=mysql;Port=3306;Database=track;Uid=root;Pwd=${MYSQL_ROOT_PASSWORD};CharSet=utf8mb4;"
},
"Jwt": {
"SecretKey": "${JWT_SECRET_KEY}",
"Issuer": "TrackCenter.Api",
"Audience": "TrackCenter.Api",
"ExpiryInMinutes": "480",
"RefreshExpiryInDays": "7",
"ClockSkewMinutes": "5"
}
}

View File

@ -0,0 +1,59 @@
#!/usr/bin/env bash
# 从 Consul 导出 KV 到 idwms/*.json(首次建仓 / 对账用)
# 注意:导出会含明文密码,勿直接 commit;先人工脱敏或改用占位符
# 用法: ./scripts/export-from-consul.sh [admin|common|itfxapi|trackapi ...]
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}"
IDWMS_DIR="${ROOT}/idwms"
mkdir -p "$IDWMS_DIR"
need() { command -v "$1" >/dev/null || { echo "FAIL: 需要 $1"; exit 1; }; }
need kubectl
need curl
need jq
if [[ -z "${CONSUL_HTTP_ADDR:-}" ]]; then
CIP="$(kubectl -n "$NS" get svc consul -o jsonpath='{.spec.clusterIP}')"
CONSUL_HTTP_ADDR="http://${CIP}:8500"
fi
if [[ -z "${CONSUL_HTTP_TOKEN:-}" ]]; then
raw="$(kubectl -n "$NS" get secret consul-acl -o jsonpath='{.data.CONSUL_HTTP_TOKEN}' 2>/dev/null || true)"
if [[ -n "$raw" ]]; then
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
if [[ $# -eq 0 ]]; then
TARGETS=(common admin itfxapi trackapi)
else
TARGETS=("$@")
fi
for name in "${TARGETS[@]}"; do
key="idwms/${name}"
out="${IDWMS_DIR}/${name}.json"
echo "==> GET ${key}"
raw="$(curl -sS "${CURL_AUTH[@]}" "${CONSUL_HTTP_ADDR}/v1/kv/${key}?raw" || true)"
if [[ -z "$raw" ]]; then
echo "WARN: ${key} 为空或不存在,跳过"
continue
fi
if ! jq empty <<<"$raw" 2>/dev/null; then
echo "FAIL: ${key} 不是 JSON"
exit 1
fi
jq . <<<"$raw" >"$out"
echo "OK → ${out}"
done
echo "导出完成。提交前请把密码改成 \${MYSQL_ROOT_PASSWORD} 等占位符。"

158
scripts/sync-to-consul.sh Normal file
View File

@ -0,0 +1,158 @@
#!/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 jq
command -v envsubst >/dev/null || { echo "FAIL: 需要 envsubst(gettext)"; exit 1; }
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 ! jq empty "${TMP}/${name}.json" 2>/dev/null; then
echo "FAIL: ${name}.json 渲染后不是合法 JSON"
exit 1
fi
# 禁止未替换的占位符漏网
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
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"