#!/usr/bin/env bash # Self-contained Docker deployment regression. Requires Linux Docker because # the validator uses the host network to reach the ephemeral HTTPS RRDP server. set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" work_root="$(mktemp -d)" server_pid="" daemon_container="" cleanup() { [[ -n "$daemon_container" ]] && docker stop --time 5 "$daemon_container" >/dev/null 2>&1 || true [[ -n "$server_pid" ]] && kill "$server_pid" 2>/dev/null || true [[ -n "$server_pid" ]] && wait "$server_pid" 2>/dev/null || true rm -rf "$work_root" } trap cleanup EXIT port="$(python3 - <<'PY' import socket with socket.socket() as sock: sock.bind(("127.0.0.1", 0)) print(sock.getsockname()[1]) PY )" fixture="$work_root/fixture" python3 "$repo_root/tests/support/generate_repository.py" \ --rrdp-host 127.0.0.1 --rrdp-port "$port" --output "$fixture" ln -s "cases/baseline-v1/http" "$fixture/active" mkdir -p "$work_root/output-snapshot" "$work_root/output-delta" "$work_root/state" PYTHONUNBUFFERED=1 python3 "$repo_root/tests/support/serve_repository.py" \ "$fixture/active" "$port" "$fixture/certs/rrdp-server.pem" "$fixture/certs/rrdp-server.key" \ >"$work_root/server.log" 2>&1 & server_pid="$!" for _ in $(seq 1 50); do rg -qx 'ready' "$work_root/server.log" && break sleep 0.1 done rg -qx 'ready' "$work_root/server.log" docker build -f "$repo_root/docker/Dockerfile" -t panda-rpki:synthetic-e2e "$repo_root" run_validator() { local out="$1" local mode="$2" docker run --rm --network host --user "$(id -u):$(id -g)" \ -v "$fixture:/fixture:ro" \ -v "$work_root/state:/state" \ -v "$out:/output" \ panda-rpki:synthetic-e2e validate \ --tal /fixture/tal/custom.tal --ta /fixture/ta/custom-ta.cer \ --http-root-cert /fixture/certs/rrdp-ca.pem \ --http-timeout-secs 5 \ --parallel-max-repo-sync-workers-global 2 \ --parallel-phase2-object-workers 2 \ --rrdp-state-dir /state --rrdp-sync-mode "$mode" \ --ccr-out /output/result.ccr --out /output --log-level info --log-format json } run_validator "$work_root/output-snapshot" snapshot rm "$fixture/active" ln -s "cases/baseline-v2/http" "$fixture/active" run_validator "$work_root/output-delta" delta for output in "$work_root/output-snapshot" "$work_root/output-delta"; do test -s "$output/summary.json" test -s "$output/vrps.csv" test -s "$output/result.ccr" openssl asn1parse -inform DER -in "$output/result.ccr" -noout done test -s "$work_root/state/repository-db/CURRENT" python3 - "$work_root/output-snapshot" "$work_root/output-delta" <<'PY' import json import pathlib import sys snapshot, delta = (pathlib.Path(item) for item in sys.argv[1:]) first = json.loads((snapshot / "summary.json").read_text(encoding="utf-8")) second = json.loads((delta / "summary.json").read_text(encoding="utf-8")) assert first["vrps"] == 1, first assert second["vrps"] == 2, second assert len((snapshot / "vaps.csv").read_text(encoding="utf-8").splitlines()) == 2 assert len((delta / "vaps.csv").read_text(encoding="utf-8").splitlines()) == 2 timing = json.loads((delta / "stage-timing.json").read_text(encoding="utf-8")) assert timing["analysis_counts"]["rrdp_delta_ops_applied_total"] > 0, timing PY echo "Synthetic Docker snapshot+delta E2E passed" # Exercise the actual public daemon in the image with a fresh protocol root. mkdir "$work_root/daemon" rm "$fixture/active" ln -s "cases/baseline-v1/http" "$fixture/active" daemon_container="panda-rpki-daemon-$(basename "$work_root")" docker run --rm --name "$daemon_container" --network host --read-only --tmpfs /tmp \ --user "$(id -u):$(id -g)" \ -v "$fixture:/fixture:ro" -v "$work_root/daemon:/data" \ panda-rpki:synthetic-e2e daemon \ --state-root /data --max-runs 3 --interval-secs 2 --retain-runs 3 -- \ --tal /fixture/tal/custom.tal --ta /fixture/ta/custom-ta.cer \ --http-root-cert /fixture/certs/rrdp-ca.pem --log-format json \ >"$work_root/daemon-container.log" 2>&1 & daemon_job=$! python3 - "$work_root/daemon" "$fixture" <<'PY' import json import pathlib import sys import time root, fixture = map(pathlib.Path, sys.argv[1:]) for _ in range(400): status = root / "daemon-status.json" if status.exists() and json.loads(status.read_text())["state"] == "sleeping": (fixture / "active").unlink() (fixture / "active").symlink_to("cases/baseline-v2/http") break time.sleep(0.05) else: raise AssertionError("Docker daemon did not finish snapshot") PY wait "$daemon_job" daemon_container="" python3 - "$work_root/daemon" <<'PY' import json import pathlib import subprocess import sys root = pathlib.Path(sys.argv[1]) assert json.loads((root / "daemon-status.json").read_text())["runs_completed"] == 3 for seq, mode, vrps in [(1, "snapshot", 1), (2, "delta", 2), (3, "delta", 2)]: out = root / f"runs/run_{seq:06}" run = json.loads((out / "run-summary.json").read_text()) assert run["status"] == "success" and run["sync_mode"] == mode, run assert run["summary"]["vrps"] == vrps, run assert run["process_metrics"]["maxRssKb"] > 0, run assert len((out / "vrps.csv").read_text().splitlines()) - 1 == vrps subprocess.run(["openssl", "asn1parse", "-inform", "DER", "-in", str(out / "result.ccr"), "-noout"], check=True) timing = json.loads((root / "runs/run_000002/stage-timing.json").read_text()) assert timing["analysis_counts"]["rrdp_delta_ops_applied_total"] > 0 print("Synthetic Docker daemon 3-run E2E passed") PY