# =================================================================== # 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