Files
loop-ops/db/verify-one.sh
T
2026-10-27 14:29:43 +03:00

128 lines
4.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Compare Users/Channels/Posts counts: source MDB vs target (CNPG or Selectel).
# CNPG cluster: registry.target_cluster / logs/<id>.cnpg-cluster / CLI profile.
# Usage: verify-one.sh <kube_context> <installation_id|uuid> [cnpg|selectel|loop-cnpg-lm|…]
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${DIR}/lib.sh"
[[ $# -ge 2 ]] || die "usage: $0 <kube_context> <installation_id|uuid> [cnpg|selectel|loop-cnpg-lm|loop-cnpg-wilix-1|loop-cnpg-ff]"
CTX="$1"
INSTALLATION_RAW="$2"
shift 2
parse_target_dsn_args "$@"
if [[ -n "${PARSE_TARGET}" ]]; then
TARGET="${PARSE_TARGET}"
fi
if [[ -n "${PARSE_CNPG_PROFILE:-}" ]]; then
CNPG_CLUSTER_PROFILE="${PARSE_CNPG_PROFILE}"
fi
INSTALLATION="$(resolve_installation "$CTX" "$INSTALLATION_RAW")"
if [[ -z "${PARSE_TARGET:-}" ]]; then
TARGET="$(load_installation_target "$INSTALLATION")"
fi
resolve_cnpg_cluster_profile "$INSTALLATION"
resolve_target "${TARGET:-cnpg}"
SOURCE_FILE="${DIR}/${LOG_DIR}/${INSTALLATION}.source.dsn"
if [[ -n "${SOURCE_DSN:-}" ]]; then
parse_dsn "$SOURCE_DSN"
elif [[ -f "$SOURCE_FILE" ]]; then
parse_dsn "$(cat "$SOURCE_FILE")"
else
die "No source DSN; run migrate-one first or set SOURCE_DSN="
fi
DB_NAME="$SRC_DB"
log "=== verify-one ${INSTALLATION} target=${TARGET_KIND} cnpg=${CNPG_CLUSTER_PROFILE:-${CNPG_CLUSTER:-?}} endpoint=${TARGET_HOST}:${TARGET_PORT} admin=${CNPG_KUBE_CONTEXT}/${CNPG_NAMESPACE}/${CNPG_CLUSTER} ==="
log "${TARGET_KIND} counts for db=${DB_NAME}"
tgt="$(target_counts "$CTX" "$INSTALLATION" "$DB_NAME" "$SRC_USER" "$SRC_PASS")"
log "${TARGET_KIND} users/channels/posts: ${tgt}"
JOB_NAME="pgverify-${INSTALLATION:0:18}-$(date +%s | tail -c 4)"
SECRET_NAME="${JOB_NAME}-env"
JOB_FILE="$(mktemp)"
cleanup() {
kubectl --context "$CTX" -n "$JOB_NS" delete job "$JOB_NAME" --ignore-not-found --wait=false >/dev/null 2>&1 || true
kubectl --context "$CTX" -n "$JOB_NS" delete secret "$SECRET_NAME" --ignore-not-found --wait=false >/dev/null 2>&1 || true
rm -f "$JOB_FILE"
}
trap cleanup EXIT
kubectl --context "$CTX" -n "$JOB_NS" create secret generic "$SECRET_NAME" \
--from-literal=SRC_HOST="$SRC_HOST" \
--from-literal=SRC_PORT="$SRC_PORT" \
--from-literal=SRC_USER="$SRC_USER" \
--from-literal=SRC_PASS="$SRC_PASS" \
--from-literal=SRC_DB="$SRC_DB" \
--from-literal=SRC_SSLMODE="${SRC_SSLMODE}" \
--dry-run=client -o yaml | kubectl --context "$CTX" -n "$JOB_NS" apply -f -
JOB_NAME="$JOB_NAME" JOB_NS="$JOB_NS" PG_IMAGE="$PG_IMAGE" SECRET_NAME="$SECRET_NAME" \
python3 - "$JOB_FILE" <<'PY'
import json, os, sys
script = r"""set -euo pipefail
export PGSSLMODE="${SRC_SSLMODE:-disable}"
PGPASSWORD="${SRC_PASS}" psql -h "${SRC_HOST}" -p "${SRC_PORT}" -U "${SRC_USER}" -d "${SRC_DB}" -Atc \
"SELECT (SELECT count(*) FROM users)||E'\t'||(SELECT count(*) FROM channels)||E'\t'||(SELECT count(*) FROM posts);"
"""
job = {
"apiVersion": "batch/v1",
"kind": "Job",
"metadata": {"name": os.environ["JOB_NAME"], "namespace": os.environ["JOB_NS"]},
"spec": {
"ttlSecondsAfterFinished": 600,
"backoffLimit": 1,
"template": {"spec": {
"restartPolicy": "Never",
"containers": [{
"name": "verify",
"image": os.environ["PG_IMAGE"],
"envFrom": [{"secretRef": {"name": os.environ["SECRET_NAME"]}}],
"command": ["bash", "-c"],
"args": [script],
}],
}},
},
}
json.dump(job, open(sys.argv[1], "w"))
PY
kubectl --context "$CTX" -n "$JOB_NS" apply -f "$JOB_FILE"
MAX_POST_DELTA="${VERIFY_MAX_POST_DELTA:-100}"
mark_verified() {
registry_set "$INSTALLATION" "status=verified" || true
if [[ "${TARGET_KIND}" == "cnpg" && -n "${CNPG_CLUSTER_PROFILE:-}" ]]; then
registry_set "$INSTALLATION" "target_cluster=${CNPG_CLUSTER_PROFILE}" || true
fi
}
if wait_job "$CTX" "$JOB_NS" "$JOB_NAME" 600; then
src="$(kubectl --context "$CTX" -n "$JOB_NS" logs "job/${JOB_NAME}" | tail -1)"
log "MDB users/channels/posts: ${src}"
if [[ "$src" == "$tgt" ]]; then
log "VERIFY OK counts match exactly"
mark_verified
exit 0
fi
python3 - "$src" "$tgt" "$MAX_POST_DELTA" <<'PY'
import sys
src, tgt, maxd = sys.argv[1], sys.argv[2], int(sys.argv[3])
su,sc,sp = map(int, src.split("\t"))
tu,tc,tp = map(int, tgt.split("\t"))
if su!=tu or sc!=tc:
print(f"users/channels mismatch src={src} tgt={tgt}", file=sys.stderr); sys.exit(2)
if abs(sp-tp) > maxd:
print(f"posts delta {abs(sp-tp)} > {maxd}", file=sys.stderr); sys.exit(2)
print(f"VERIFY OK users/channels match; posts delta={abs(sp-tp)} (live source)")
PY
mark_verified
exit 0
fi
die "verify source job failed"