teleo-infrastructure/ops/run_sqlite_postgres_restore_canary.sh
twentyOne2x 2b4423e3d1
Some checks are pending
CI / lint-and-test (push) Waiting to run
Add SQLite to Postgres restore canary (#43)
2026-07-07 12:21:54 +02:00

139 lines
4.8 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
# Restores a Teleo SQLite backup into a disposable local Postgres container and
# compares source/target table row counts. It does not touch GCP or production.
SQLITE_BACKUP="${SQLITE_BACKUP:-}"
POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgres:16-alpine}"
OUTPUT_ROOT="${OUTPUT_ROOT:-./outputs/gcp-infra-hardening-20260707/private-restore-canaries}"
PROOF_ROOT="${PROOF_ROOT:-./outputs/gcp-infra-hardening-20260707/proofs}"
TS="${TS:-$(date -u +%Y%m%dT%H%M%SZ)}"
SCHEMA="${SCHEMA:-teleo_restore}"
CONTAINER_NAME="${CONTAINER_NAME:-teleo-pg-restore-canary-${TS}}"
if [[ -z "$SQLITE_BACKUP" ]]; then
echo "SQLITE_BACKUP is required, for example SQLITE_BACKUP=outputs/.../teleo-pipeline-sqlite-...db.gz" >&2
exit 2
fi
if [[ ! -f "$SQLITE_BACKUP" ]]; then
echo "SQLite backup not found: $SQLITE_BACKUP" >&2
exit 2
fi
mkdir -p "$OUTPUT_ROOT" "$PROOF_ROOT"
chmod 700 "$OUTPUT_ROOT"
WORK_DIR="$OUTPUT_ROOT/sqlite-postgres-restore-canary-${TS}"
mkdir -p "$WORK_DIR"
chmod 700 "$WORK_DIR"
IMPORT_SQL="$WORK_DIR/teleo-pipeline-postgres-import.sql"
SOURCE_SUMMARY="$WORK_DIR/source-summary.json"
TARGET_COUNTS_SQL="$WORK_DIR/target-counts.sql"
TARGET_COUNTS_CSV="$WORK_DIR/target-counts.csv"
IMPORT_LOG="$WORK_DIR/postgres-import.log"
PROOF_JSON="$PROOF_ROOT/sqlite-postgres-restore-canary-${TS}.json"
cleanup() {
if [[ "${KEEP_CONTAINER:-0}" != "1" ]]; then
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
python3 ops/sqlite_to_postgres_dump.py \
--sqlite-backup "$SQLITE_BACKUP" \
--output-sql "$IMPORT_SQL" \
--summary-json "$SOURCE_SUMMARY" \
--schema "$SCHEMA" \
--target-counts-sql "$TARGET_COUNTS_SQL"
docker run --rm -d \
--name "$CONTAINER_NAME" \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=teleo_restore_canary \
"$POSTGRES_IMAGE" >/dev/null
for _ in $(seq 1 60); do
if docker exec "$CONTAINER_NAME" pg_isready -U postgres -d teleo_restore_canary >/dev/null 2>&1; then
break
fi
sleep 1
done
docker exec "$CONTAINER_NAME" pg_isready -U postgres -d teleo_restore_canary >/dev/null
docker exec -i "$CONTAINER_NAME" \
psql -v ON_ERROR_STOP=1 -U postgres -d teleo_restore_canary \
< "$IMPORT_SQL" > "$IMPORT_LOG"
docker exec -i "$CONTAINER_NAME" \
psql -v ON_ERROR_STOP=1 -U postgres -d teleo_restore_canary \
< "$TARGET_COUNTS_SQL" > "$TARGET_COUNTS_CSV"
python3 - "$SOURCE_SUMMARY" "$TARGET_COUNTS_CSV" "$PROOF_JSON" "$SQLITE_BACKUP" "$IMPORT_SQL" "$POSTGRES_IMAGE" <<'PY'
import csv
import hashlib
import json
import sys
from datetime import UTC, datetime
from pathlib import Path
source_summary_path = Path(sys.argv[1])
target_counts_path = Path(sys.argv[2])
proof_path = Path(sys.argv[3])
sqlite_backup = Path(sys.argv[4])
import_sql = Path(sys.argv[5])
postgres_image = sys.argv[6]
source_summary = json.loads(source_summary_path.read_text())
source_counts = {table["name"]: int(table["row_count"]) for table in source_summary["tables"]}
with target_counts_path.open(newline="") as handle:
target_counts = {row["table_name"]: int(row["row_count"]) for row in csv.DictReader(handle)}
all_tables = sorted(set(source_counts) | set(target_counts))
mismatches = [
{
"table": table,
"source_rows": source_counts.get(table),
"target_rows": target_counts.get(table),
}
for table in all_tables
if source_counts.get(table) != target_counts.get(table)
]
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
proof = {
"artifact": "sqlite_postgres_restore_canary",
"generated_at_utc": datetime.now(UTC).isoformat(),
"status": "pass" if not mismatches and source_summary["source_integrity_check"] == "ok" else "fail",
"source_sqlite_backup": str(sqlite_backup),
"source_sqlite_backup_sha256": sha256(sqlite_backup),
"postgres_image": postgres_image,
"source_integrity_check": source_summary["source_integrity_check"],
"postgres_schema": source_summary["schema"],
"source_table_count": len(source_counts),
"target_table_count": len(target_counts),
"source_total_rows": sum(source_counts.values()),
"target_total_rows": sum(target_counts.values()),
"mismatched_tables": mismatches,
"conversion_notes": source_summary["conversion_notes"],
"conversion_stats": source_summary["conversion_stats"],
"private_artifacts": {
"source_summary_json": str(source_summary_path),
"postgres_import_sql": str(import_sql),
"target_counts_csv": str(target_counts_path),
},
}
proof_path.write_text(json.dumps(proof, indent=2, sort_keys=True) + "\n")
print(json.dumps(proof, indent=2, sort_keys=True))
sys.exit(0 if proof["status"] == "pass" else 1)
PY