#!/usr/bin/env bash # Compare Users/Channels/Posts counts: source MDB vs target (CNPG or Selectel). # CNPG cluster: registry.target_cluster / logs/.cnpg-cluster / CLI profile. # Usage: verify-one.sh [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 [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"