topfans/k8s/jobs/pg-sequence-sync/job-template.yaml
2026-07-06 13:26:46 +08:00

139 lines
4.8 KiB
YAML

# ===================================================================
# PostgreSQL 序列同步 Job 模板
# ===================================================================
# 这个是"裸"模板, 部署时 helm hook 在 pre-install / pre-upgrade 时跑一次。
# Chart 内部 templates/ 也有一个对应的模板走 helm hook, 两种用法:
#
# 1. helm install/upgrade 自动触发 (推荐)
# → 见 templates/pg-sequence-sync.yaml (已挂 helm hook)
#
# 2. 单独手动跑 (紧急 / 调试 / 补单)
# → kubectl apply -f k8s/jobs/pg-sequence-sync/manual-job.yaml
# (manual-job.yaml 从本模板渲染, 把 ${TABLES_CSV} 替换成实际值)
#
# ⚠️ 切流量前必跑 (CLAUDE.md 强制规范, 设计文档 §10.3 hard blocker)
# ===================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: topfans-pg-sequence-sync-script
namespace: topfans
labels:
app.kubernetes.io/component: pg-sequence-sync
app.kubernetes.io/part-of: topfans
data:
# 完全展开后的脚本: kubectl create cm -n topfans --from-file=script.sh=script.sh
script.sh: |
#!/bin/bash
set -euo pipefail
PG_HOST="${PG_HOST:-postgres}"
PG_PORT="${PG_PORT:-5432}"
PG_USER="${PG_USER:-postgres}"
PG_DB="${PG_DB:-topfans}"
PG_PASSWORD="${PG_PASSWORD:?PG_PASSWORD 必须通过 envFrom secret 注入}"
TABLES_CSV="${TABLES_CSV:-assets,asset_registry,users,stars,activity_assets,collection_assets,materials,exhibitions,galleries}"
IFS=',' read -ra TABLES <<< "$TABLES_CSV"
echo "Tables to sync: ${#TABLES[@]}"
for tbl in "${TABLES[@]}"; do
tbl=$(echo "$tbl" | tr -d ' ')
[ -z "$tbl" ] && continue
max_id=$(PGPASSWORD="$PG_PASSWORD" psql \
-h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
-tAc "SELECT COALESCE(MAX(id), 0) FROM ${tbl};")
[ "$max_id" = "0" ] && { echo " ${tbl}: empty, skip"; continue; }
PGPASSWORD="$PG_PASSWORD" psql \
-h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
-c "SELECT setval('${tbl}_id_seq', ${max_id}, true);"
echo " ${tbl}: setval(${tbl}_id_seq, ${max_id}, true)"
done
# Verify
bad=0
for tbl in "${TABLES[@]}"; do
tbl=$(echo "$tbl" | tr -d ' ')
[ -z "$tbl" ] && continue
h=$(PGPASSWORD="$PG_PASSWORD" psql \
-h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
-tAc "SELECT last_value >= COALESCE((SELECT MAX(id) FROM ONLY ${tbl}), 0) FROM pg_sequences WHERE sequencename = '${tbl}_id_seq';")
if [ "$h" != "t" ]; then
echo " ❌ ${tbl}_id_seq unhealthy"
bad=$((bad+1))
else
echo " ✅ ${tbl}_id_seq healthy"
fi
done
[ "$bad" -gt 0 ] && { echo "❌ ${bad} unhealthy"; exit 1; }
echo "✅ all healthy"
---
apiVersion: v1
kind: Secret
metadata:
name: pg-sequence-sync-pg-password
namespace: topfans
labels:
app.kubernetes.io/component: pg-sequence-sync
type: Opaque
stringData:
PG_PASSWORD: "" # ← 必须从 db-credentials 同步或 CI 注入
---
apiVersion: batch/v1
kind: Job
metadata:
name: topfans-pg-sequence-sync
namespace: topfans
labels:
app.kubernetes.io/component: pg-sequence-sync
app.kubernetes.io/part-of: topfans
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5" # 在 oss-cors-init (-10) 之后跑, 但顺序不重要
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
ttlSecondsAfterFinished: 600
backoffLimit: 0
template:
spec:
restartPolicy: Never
# postgresql-client 镜像: 用 bitnami/postgresql:latest 仅 client 部分, 不带 server
# 或 alpine + apk add postgresql-client。 这里用 Google 维护的轻量镜像
containers:
- name: psql
image: alpine:3.19
command: ["/bin/sh", "-c"]
args:
- |
set -e
apk add --no-cache postgresql16-client bash >/dev/null
bash /scripts/script.sh
env:
- name: PG_HOST
value: "postgres"
- name: PG_PORT
value: "5432"
- name: PG_USER
value: "postgres"
- name: PG_DB
value: "topfans"
- name: PG_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
- name: TABLES_CSV
value: "assets,asset_registry,users,stars,activity_assets,collection_assets,materials,exhibitions,galleries"
volumeMounts:
- name: script
mountPath: /scripts
readOnly: true
volumes:
- name: script
configMap:
name: topfans-pg-sequence-sync-script
defaultMode: 0o755
securityContext:
runAsNonRoot: true
runAsUser: 65534 # nobody