Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 1m59s
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b
216 lines
10 KiB
Python
216 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Rehearse the accepted-cohort date patch on isolated recovery data only.
|
|
|
|
The SQL transaction always rolls back. File changes affect a disposable copy;
|
|
the recovered pair and production are never changed. This is not a live executor.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
CHECKS = r'''
|
|
import copy, hashlib, json, django
|
|
from pathlib import Path
|
|
django.setup()
|
|
from django.db import connection, transaction
|
|
from coulomb_social.apps.spaces.models import Space
|
|
from coulomb_social.apps.spaces.date_backfill import prepare_index_date_patch
|
|
from coulomb_social.apps.spaces.bubble_transfer import source_timestamp
|
|
|
|
assert connection.vendor == "postgresql"
|
|
plan = json.loads(Path("/review/plan.json").read_bytes())
|
|
selection = plan["selection"]
|
|
root = Path("/rehearsal-content")
|
|
index = root / "spaces" / selection["space_slug"] / "pages/index.md"
|
|
original = index.read_bytes()
|
|
patched = prepare_index_date_patch(plan, original)
|
|
|
|
def files():
|
|
return {str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest()
|
|
for p in root.rglob("*") if p.is_file()}
|
|
|
|
def database():
|
|
result = {}
|
|
with connection.cursor() as cursor:
|
|
cursor.execute("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY 1")
|
|
tables = [r[0] for r in cursor.fetchall()]
|
|
for table in tables:
|
|
cursor.execute("SELECT * FROM " + connection.ops.quote_name(table))
|
|
columns = [c[0] for c in cursor.description]
|
|
result[table] = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
|
return result
|
|
|
|
def digest(rows):
|
|
return {table: sorted(json.dumps(row, sort_keys=True, default=str) for row in entries)
|
|
for table, entries in rows.items()}
|
|
|
|
before_files = files()
|
|
with transaction.atomic():
|
|
target = Space.objects.select_for_update().get(
|
|
pk=selection["space_id"], tenant_id=selection["tenant_id"], slug=selection["space_slug"])
|
|
prior = source_timestamp(plan["observed_sql_created_at"])
|
|
desired = source_timestamp(plan["source_created_at"])
|
|
assert target.created_at == prior
|
|
pages = root / "spaces" / selection["space_slug"] / "pages"
|
|
observed = {p["slug"]: p["observed_sha256"] for p in plan["pages"]}
|
|
assert len(observed) == len(plan["pages"])
|
|
observed["index"] = plan["index_sha256"]
|
|
assert {p.stem: hashlib.sha256(p.read_bytes()).hexdigest()
|
|
for p in pages.glob("*.md")} == observed
|
|
before = database()
|
|
expected = copy.deepcopy(before)
|
|
selected = [r for r in expected["spaces_space"] if r["id"] == target.pk]
|
|
assert len(selected) == 1
|
|
selected[0]["created_at"] = desired
|
|
try:
|
|
assert Space.objects.filter(pk=target.pk, created_at=prior).update(created_at=desired) == 1
|
|
index.write_bytes(patched)
|
|
after_files = dict(before_files)
|
|
after_files[str(index.relative_to(root))] = hashlib.sha256(patched).hexdigest()
|
|
assert digest(database()) == digest(expected)
|
|
assert files() == after_files
|
|
assert Space.objects.get(pk=target.pk).created_at == desired
|
|
# Replaying the old SQL precondition must refuse, rather than rewrite.
|
|
if prior != desired:
|
|
assert Space.objects.filter(pk=target.pk, created_at=prior).update(
|
|
created_at=desired) == 0
|
|
assert Space.objects.filter(pk=target.pk, created_at=desired).update(created_at=prior) == 1
|
|
index.write_bytes(original)
|
|
assert digest(database()) == digest(before) and files() == before_files
|
|
# Prove rollback after the file has been staged and SQL has changed.
|
|
Space.objects.filter(pk=target.pk).update(created_at=desired)
|
|
index.write_bytes(patched)
|
|
finally:
|
|
index.write_bytes(original)
|
|
transaction.set_rollback(True)
|
|
assert digest(database()) == digest(before) and files() == before_files
|
|
print(json.dumps({
|
|
"status": "recovered_date_patch_rehearsed",
|
|
"business_tables": len(before) - int("django_migrations" in before),
|
|
"all_table_rows_compared": True,
|
|
"sql_changes": int(prior != desired), "content_files": len(before_files),
|
|
"index_changed": patched != original,
|
|
"only_selected_date_and_index_changed": True,
|
|
"reverse_restores_all_rows_and_files": True,
|
|
"sql_rollback_and_file_cleanup_verified": True,
|
|
"replay_precondition_refused": prior != desired,
|
|
"other_page_dates_and_bytes_preserved": len(observed) - 1,
|
|
"grants_and_local_pages_unchanged": True,
|
|
}))
|
|
'''
|
|
|
|
|
|
def run(arguments):
|
|
return subprocess.run(arguments, capture_output=True, timeout=180, check=True).stdout
|
|
|
|
|
|
def hashes(root):
|
|
entries = list(root.rglob("*"))
|
|
if any(p.is_symlink() for p in entries):
|
|
raise ValueError("Recovery content must not contain symlinks")
|
|
return {str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest()
|
|
for p in entries if p.is_file()}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--restore-receipt", type=Path, required=True)
|
|
parser.add_argument("--plan", type=Path, required=True)
|
|
parser.add_argument("--candidate-image", required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
if args.output.exists() or args.output.is_symlink():
|
|
raise ValueError("Fresh output required")
|
|
state = json.loads(args.restore_receipt.read_bytes())
|
|
if (state["status"] != "restored_verified"
|
|
or not re.fullmatch(r"csoc-recovery-\d{14}", state["database_container"])
|
|
or state["network"] != state["database_container"] + "-net"):
|
|
raise ValueError("Isolated recovery receipt required")
|
|
network = json.loads(run(["docker", "network", "inspect", state["network"]]))[0]
|
|
db = json.loads(run(["docker", "inspect", state["database_container"]]))[0]
|
|
if (network["Internal"] is not True or not db["State"]["Running"]
|
|
or set(db["NetworkSettings"]["Networks"]) != {state["network"]}
|
|
or any(db["NetworkSettings"]["Ports"].values())
|
|
or db["Image"] != state["postgres_image_id"]):
|
|
raise ValueError("Recovery isolation mismatch")
|
|
image = run(["docker", "image", "inspect", args.candidate_image,
|
|
"--format", "{{.Id}}"]).decode().strip()
|
|
if not re.fullmatch(r"sha256:[a-f0-9]{64}", image):
|
|
raise ValueError("Locally retained image required")
|
|
root = Path(state["restored_directory"]) / "content"
|
|
if root.is_symlink() or not root.resolve().is_relative_to(Path("/tmp")):
|
|
raise ValueError("Temporary recovered content required")
|
|
before = hashes(root)
|
|
module = Path(__file__).resolve().parents[1] / "coulomb_social/apps/spaces/date_backfill.py"
|
|
module_bytes, plan_bytes = module.read_bytes(), args.plan.read_bytes()
|
|
with tempfile.TemporaryDirectory(prefix="csoc-date-rehearsal-") as temporary:
|
|
work = Path(temporary)
|
|
shutil.copytree(root, work / "content")
|
|
(work / "plan.json").write_bytes(plan_bytes)
|
|
(work / "date_backfill.py").write_bytes(module_bytes)
|
|
name = "csoc-date-check-" + uuid.uuid4().hex
|
|
command = [
|
|
"docker", "run", "--rm", "--name", name, "--network", state["network"],
|
|
"--read-only", "--tmpfs", "/tmp", "--cap-drop=ALL",
|
|
"--security-opt=no-new-privileges", "--user", f"{os.getuid()}:{os.getgid()}",
|
|
"--entrypoint", "/app/.venv/bin/python",
|
|
"-e", "DJANGO_SETTINGS_MODULE=coulomb_social.settings.base",
|
|
"-e", "DATABASE_URL=postgres://postgres@recovery-db/coulomb_social_restore",
|
|
"-e", "CONTENT_ROOT=/rehearsal-content", "-e", "FORGEJO_BASE_URL=",
|
|
"-e", "PYTHONDONTWRITEBYTECODE=1",
|
|
"-v", str(work / "content") + ":/rehearsal-content",
|
|
"-v", str(work / "plan.json") + ":/review/plan.json:ro",
|
|
"-v", str(work / "date_backfill.py")
|
|
+ ":/app/coulomb_social/apps/spaces/date_backfill.py:ro",
|
|
image, "-c", CHECKS,
|
|
]
|
|
try:
|
|
result = subprocess.run(command, capture_output=True, timeout=180)
|
|
finally:
|
|
# Killing the Docker client alone could leave the transaction open.
|
|
# Remove this exact disposable client container, never the database.
|
|
subprocess.run(["docker", "rm", "-f", name], capture_output=True,
|
|
timeout=30, check=False)
|
|
if result.returncode:
|
|
fd, diagnostic = tempfile.mkstemp(prefix="csoc-date-private-diagnostics-", dir="/tmp")
|
|
with os.fdopen(fd, "wb") as stream:
|
|
stream.write(result.stdout + result.stderr)
|
|
print(json.dumps({"private_diagnostics": diagnostic}))
|
|
raise ValueError("Rehearsal refused; inspect private diagnostics locally")
|
|
rows = [json.loads(line) for line in result.stdout.splitlines() if line.startswith(b"{")]
|
|
if len(rows) != 1 or hashes(root) != before or hashes(work / "content") != before:
|
|
raise ValueError("Recovery preservation check failed")
|
|
proof = {
|
|
"schema": "coulomb.source-date-backfill-rehearsal.v1", **rows[0],
|
|
"candidate_image_id": image,
|
|
"date_patch_module_sha256": hashlib.sha256(module_bytes).hexdigest(),
|
|
"rehearsal_runner_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
|
"private_plan_sha256": hashlib.sha256(plan_bytes).hexdigest(),
|
|
"network_internal": True, "published_ports": 0,
|
|
"original_recovered_files_unchanged": True,
|
|
"production_access": False, "production_writes": False,
|
|
"sql_commit": False, "source_overlay_used": True,
|
|
"live_executor_prepared": False,
|
|
}
|
|
fd = os.open(args.output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
with os.fdopen(fd, "w") as stream:
|
|
json.dump(proof, stream, indent=2)
|
|
stream.write("\n")
|
|
print(json.dumps(proof))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except (ValueError, OSError, KeyError, subprocess.SubprocessError):
|
|
raise SystemExit("Date rehearsal refused; inspect private inputs/diagnostics locally.")
|