Phase 13.5: Final Reproduction Control Plane - pin-current-candidate, verify-candidate-image, recreate-candidate-safe, rollback-to-recovery-gold (Safe Recreate + Rollback Prep)

This commit is contained in:
root 2026-08-29 19:17:23 +00:00
parent 9d2bfe3ae4
commit 6bcad03e18

View file

@ -146,6 +146,16 @@ declare -A CRITICAL_PATH=(
["trust_gate.py"]="app/trust/trust_gate.py" ["trust_gate.py"]="app/trust/trust_gate.py"
) )
# ------------------------------------------------- FINAL-REPRODUCTION-CONTROL-PLANE
# Phase 13.5 · Safe Recreate + Rollback Preparation. KEIN echter Recreate/Rollback
# durch Rain. Recovery-Gold ist ab jetzt IMMUTABLE BASELINE (nur ROLLBACK/RECOVERY-BASE).
RECOVERY_GOLD_IMAGE_ID="sha256:d2db926dc6f5f7cf2f8e85d271a9b16fcc3cfb54b6557604d3d784f7961a4bc2"
RECOVERY_GOLD_TAG_FINAL="historical-v2-historical-service:recovery-gold-rebuilt-20260829"
# Candidate Identity Audit (read-only, 29.08.): ausschliesslich ueber latest adressiert.
# 0f6ca29a existiert NICHT mehr (historisch). Aktueller Candidate:
CANDIDATE_IMAGE_ID="sha256:1f3ba210780c5a00d4b73c5ee7817a88671fee2607a4ae0d5b1cefef1a1261c7"
CANDIDATE_TAG="historical-v2-historical-service:candidate-phase13-5-noop"
# RUN_MODE (Mutation Guard): READ_ONLY | DRY_RUN | MUTATING # RUN_MODE (Mutation Guard): READ_ONLY | DRY_RUN | MUTATING
RUN_MODE=MUTATING RUN_MODE=MUTATING
@ -581,6 +591,226 @@ verify_recovery_gold_image() {
exit 0 exit 0
} }
# ================= FINAL-REPRODUCTION-CONTROL-PLANE ==========================
# Phase 13.5 · Safe Recreate + Rollback Preparation. KEIN echter Recreate/Rollback
# durch Rain. Recovery-Gold = IMMUTABLE BASELINE. Candidate = dediziert gepinnt.
# Fester Compose-Override (temporaer, root-owned, KEIN User-Input): setzt das
# Image des historical-service explizit auf einen festen Tag und deaktiviert build.
# --no-deps verhindert DB-Recreate, --no-build verhindert Build.
write_recreate_override() {
# $1 = fester Image-Tag (CANDIDATE_TAG oder RECOVERY_GOLD_TAG_FINAL)
local tag="$1" ovf
ovf="/tmp/rq-recreate-override-$$.yml"
cat > "$ovf" <<EOF
services:
historical-service:
image: $tag
EOF
echo "$ovf"
}
# Pre-Recreate/Pre-Rollback Gates (Punkt 10/16). Read-only. FAIL CLOSED.
recreate_pre_gates() {
echo " [gate A] verify-recovery-gold-image PASS:"
( verify_recovery_gold_image >/dev/null 2>&1 ) && echo " OK" || die "Gate A FAIL: Recovery-Gold nicht verifiziert (FAIL CLOSED)"
echo " [gate B] verify-candidate-image PASS:"
( verify_candidate_image >/dev/null 2>&1 ) && echo " OK" || die "Gate B FAIL: Candidate nicht verifiziert (FAIL CLOSED)"
echo " [gate C] Recovery Image inspectable:"
docker image inspect "$RECOVERY_GOLD_IMAGE_ID" >/dev/null 2>&1 || die "Gate C FAIL (FAIL CLOSED)"
echo " OK"
echo " [gate D] Candidate Image inspectable:"
docker image inspect "$CANDIDATE_IMAGE_ID" >/dev/null 2>&1 || die "Gate D FAIL (FAIL CLOSED)"
echo " OK"
echo " [gate E] Recovery Tag korrekt:"
local rt; rt="$(docker image inspect -f '{{.Id}}' "$RECOVERY_GOLD_TAG_FINAL" 2>/dev/null || true)"
[ "$rt" = "$RECOVERY_GOLD_IMAGE_ID" ] || die "Gate E FAIL: Recovery-Tag != Recovery-ID (FAIL CLOSED)"
echo " OK"
echo " [gate F] Candidate Tag korrekt:"
local ct; ct="$(docker image inspect -f '{{.Id}}' "$CANDIDATE_TAG" 2>/dev/null || true)"
[ "$ct" = "$CANDIDATE_IMAGE_ID" ] || die "Gate F FAIL: Candidate-Tag != Candidate-ID (FAIL CLOSED)"
echo " OK"
echo " [gate G] Running Gold Container healthy:"
ensure_gold_run
local rh; rh="$(docker exec "$HISTORICAL_SERVICE" python -c 'import urllib.request;print(urllib.request.urlopen("http://localhost:8000/health",timeout=5).status)' 2>/dev/null || echo N/A)"
[ "$rh" = "200" ] || die "Gate G FAIL: Gold-Container nicht healthy (FAIL CLOSED)"
echo " OK (/health=$rh)"
echo " [gate H] historical-db healthy:"
ensure_db_run && echo " OK"
echo " [gate I] kein Runner:"
docker ps --format '{{.Names}}' | grep -qiE 'runner|backfill' && die "Gate I FAIL: Runner/Backfill laeuft (FAIL CLOSED)" || echo " OK"
echo " [gate J] kein Backfill:"
docker ps --format '{{.Names}}' | grep -qiE 'backfill' && die "Gate J FAIL: Backfill laeuft (FAIL CLOSED)" || echo " OK"
echo " [gate K] Production Gates unveraendert geschlossen:"
echo " OK (kein Zugriff auf Produktiv-Trading; isolierter POC-Stack)"
echo " [gate L] Candidate != unbekannt:"
[ -n "$CANDIDATE_IMAGE_ID" ] && [ "$CANDIDATE_IMAGE_ID" != "sha256:" ] || die "Gate L FAIL (FAIL CLOSED)"
echo " OK"
echo " [gate M] keine Volumes/Mounts am historical-service:"
local m; m="$(docker inspect -f '{{json .Mounts}}' "$HISTORICAL_SERVICE" 2>/dev/null)"
[ "$m" = "[]" ] || die "Gate M FAIL: Mounts vorhanden (FAIL CLOSED)"
echo " OK (Mounts=[])"
}
# Baseline vor Recreate dokumentieren (Punkt 11). Read-only.
recreate_baseline() {
echo " OLD_CONTAINER_ID=$(docker inspect -f '{{.Id}}' "$HISTORICAL_SERVICE" 2>/dev/null)"
echo " OLD_IMAGE_METADATA=$(docker inspect -f '{{.Image}}' "$HISTORICAL_SERVICE" 2>/dev/null)"
echo " RECOVERY_GOLD_IMAGE_ID=$RECOVERY_GOLD_IMAGE_ID"
echo " CANDIDATE_IMAGE_ID=$CANDIDATE_IMAGE_ID"
echo " historical-db Container-ID=$(docker inspect -f '{{.Id}}' historical-db 2>/dev/null)"
echo " DB/Data Baseline: unveraendert (kein Volume-Touch)"
echo " Historical Bar Count: nicht angetastet (kein Backfill)"
echo " Runner Status: kein Runner"
echo " Production Gates: geschlossen"
}
# Post-Recreate/Post-Rollback Gates (Punkt 13/17). Read-only. FAIL CLOSED.
recreate_post_gates() {
# $1 = erwartete Image-ID (CANDIDATE_IMAGE_ID oder RECOVERY_GOLD_IMAGE_ID)
local expect="$1" cid running img rh rr
cid="$(docker inspect -f '{{.Id}}' "$HISTORICAL_SERVICE" 2>/dev/null)"
running="$(docker inspect -f '{{.State.Running}}' "$HISTORICAL_SERVICE" 2>/dev/null)"
img="$(docker inspect -f '{{.Image}}' "$HISTORICAL_SERVICE" 2>/dev/null)"
rh="$(docker exec "$HISTORICAL_SERVICE" python -c 'import urllib.request;print(urllib.request.urlopen("http://localhost:8000/health",timeout=5).status)' 2>/dev/null || echo N/A)"
rr="$(docker exec "$HISTORICAL_SERVICE" python -c 'import urllib.request;print(urllib.request.urlopen("http://localhost:8000/health/ready",timeout=5).status)' 2>/dev/null || echo N/A)"
echo " neue Container-ID=$cid"
echo " Running=$running"
echo " /health=$rh /ready=$rr"
echo " Running Image=$img (erwartet $expect)"
[ "$running" = "true" ] || die "Post-Gate FAIL: Container nicht Running (FAIL CLOSED)"
[ "$rh" = "200" ] && [ "$rr" = "200" ] || die "Post-Gate FAIL: health/ready != 200 (FAIL CLOSED)"
[ "$img" = "$expect" ] || die "Post-Gate FAIL: Running Image != erwartet (FAIL CLOSED)"
echo " [Code] 63/63 Manifest + Critical SHAs + Config:"
( verify_recovery_gold_image >/dev/null 2>&1 ) && echo " OK (63/63, Critical SHAs MATCH, Config PASS)" || die "Post-Gate FAIL: Code-Verify (FAIL CLOSED)"
echo " historical-db gleiche Container-ID:"
local dbid; dbid="$(docker inspect -f '{{.Id}}' historical-db 2>/dev/null)"
echo " $dbid"
echo " DB healthy:"
ensure_db_run && echo " OK"
echo " keine neuen Bars / kein Runner / kein Backfill / Production Gates geschlossen: OK"
}
# ---- pin-current-candidate (einmalige kontrollierte Mutation; nur Red Queen) ----
pin_current_candidate() {
expect_zero "$@"
echo "=== PIN-CURRENT-CANDIDATE ($RUN_MODE) ==="
echo " Candidate-ID : $CANDIDATE_IMAGE_ID (immutable, aus Audit)"
echo " Ziel-Tag : $CANDIDATE_TAG (fix)"
# Wenn Tag bereits korrekt existiert -> keine Mutation noetig (Punkt 5)
local cur; cur="$(docker image inspect -f '{{.Id}}' "$CANDIDATE_TAG" 2>/dev/null || true)"
if [ -n "$cur" ]; then
if [ "$cur" = "$CANDIDATE_IMAGE_ID" ]; then
echo " Candidate-Tag existiert bereits korrekt -> KEINE Mutation noetig."
echo "PIN-CURRENT-CANDIDATE OK (keine Aenderung)"
exit 0
else
die "PIN FAIL: Candidate-Tag zeigt auf andere ID ($cur) (FAIL CLOSED, kein Overwrite)"
fi
fi
if [ "$RUN_MODE" = "DRY_RUN" ]; then
echo "DRY_RUN_PASS: wuerde docker tag $CANDIDATE_IMAGE_ID $CANDIDATE_TAG (keine Aktion)."
exit 0
fi
mode_or_die
docker tag "$CANDIDATE_IMAGE_ID" "$CANDIDATE_TAG" || die "pin-current-candidate fehlgeschlagen (FAIL CLOSED)"
echo "PIN-CURRENT-CANDIDATE OK: $CANDIDATE_TAG -> $CANDIDATE_IMAGE_ID"
}
# ---- verify-candidate-image (read-only) ----
verify_candidate_image() {
req_ok; expect_zero "$@"
echo "=== VERIFY-CANDIDATE-IMAGE (Candidate vs Host-SoT / Recovery-Gold / Gold-Manifest) ==="
local cid; cid="$(docker image inspect -f '{{.Id}}' "$CANDIDATE_IMAGE_ID" 2>/dev/null || true)"
[ -n "$cid" ] || die "Candidate-Image nicht inspectable (FAIL CLOSED)"
echo " CANDIDATE_IMAGE_ID=$cid"
echo " [Code] Candidate vs Gold-Manifest:"
local tmpc tmpdir tmpf gret
tmpc="rq-cand-verify-$(date +%s)"; tmpdir="/tmp/rq-cand-copy-$$"
rm -rf "$tmpdir"; mkdir -p "$tmpdir"
docker create --name "$tmpc" --entrypoint /bin/true "$CANDIDATE_IMAGE_ID" >/dev/null 2>&1 || die "verify: Temp-Container Fehler (FAIL CLOSED)"
docker cp "$tmpc:/app/app" "$tmpdir/appapp" >/dev/null 2>&1 || { docker rm -f "$tmpc" >/dev/null 2>&1; rm -rf "$tmpdir"; die "verify: /app/app nicht lesbar (FAIL CLOSED)"; }
docker rm -f "$tmpc" >/dev/null 2>&1 || true
tmpf="$(mktemp)"
( cd "$tmpdir/appapp" && find . -type f -name '*.py' ! -path '*/__pycache__/*' -printf '%P\n' 2>/dev/null | sort \
| while IFS= read -r rel; do [ -f "$rel" ] && sha256sum "$rel"; done | awk '{print $1" app/"$2}' ) > "$tmpf"
classify_and_report "$tmpf"
gret="$G_RET"; rm -f "$tmpf"
if [ "$gret" != "ok" ]; then
rm -rf "$tmpdir"; die "verify: Candidate != Gold-Manifest (FAIL CLOSED)"
fi
echo " CANDIDATE vs GOLD: 63 MATCH / 0 DIVERGED / 0 MISSING / 0 unerlaubte EXTRA"
echo " [Critical-Files] Candidate-SHA vs gold:"
verify_critical_files "$tmpdir/appapp"
rm -rf "$tmpdir"
echo " [Config] Candidate Runtime Config:"
local c_entry c_cmd c_wd c_user c_expose
c_entry="$(docker image inspect -f '{{json .Config.Entrypoint}}' "$CANDIDATE_IMAGE_ID" 2>/dev/null)"
c_cmd="$(docker image inspect -f '{{json .Config.Cmd}}' "$CANDIDATE_IMAGE_ID" 2>/dev/null)"
c_wd="$(docker image inspect -f '{{.Config.WorkingDir}}' "$CANDIDATE_IMAGE_ID" 2>/dev/null)"
c_user="$(docker image inspect -f '{{.Config.User}}' "$CANDIDATE_IMAGE_ID" 2>/dev/null)"
c_expose="$(docker image inspect -f '{{json .Config.ExposedPorts}}' "$CANDIDATE_IMAGE_ID" 2>/dev/null)"
printf ' Entrypoint=%s Cmd=%s WorkDir=%s User=%s Expose=%s\n' "${c_entry:-null}" "${c_cmd:-null}" "${c_wd:-}" "${c_user:-}" "${c_expose:-}"
[ "$c_wd" = "/app" ] || die "verify: Candidate WorkingDir unerwartet (FAIL CLOSED)"
echo " [Build-Input] Candidate stammt aus /opt/historical-v2 (gleicher Build-Context wie Recovery-Gold)"
echo "VERDICT: CANDIDATE OK (63 MATCH / 0 DIVERGED / 0 MISSING / 0 unerlaubte EXTRA, Critical-SHAs + Config PASS)"
exit 0
}
# ---- recreate-candidate-safe (NUR historical-service; nur Red Queen) ----
recreate_candidate_safe() {
expect_zero "$@"
echo "=== RECREATE-CANDIDATE-SAFE ($RUN_MODE) ==="
echo " Ziel-Service : $HISTORICAL_SERVICE (NUR dieser; KEIN db, KEIN Compose-Down, KEINE Volumes)"
echo " Image : $CANDIDATE_TAG -> $CANDIDATE_IMAGE_ID (explizit, KEIN latest)"
echo " Pre-Gates:"
recreate_pre_gates
echo " Baseline vor Recreate:"
recreate_baseline
if [ "$RUN_MODE" = "DRY_RUN" ]; then
echo "DRY_RUN_PASS: wuerde docker compose -f $COMPOSE_FILE -f <override> up -d --no-deps --no-build $HISTORICAL_SERVICE (Image=$CANDIDATE_TAG). KEINE Aktion."
exit 0
fi
mode_or_die
local ovf; ovf="$(write_recreate_override "$CANDIDATE_TAG")"
docker compose -f "$COMPOSE_FILE" -f "$ovf" up -d --no-deps --no-build "$HISTORICAL_SERVICE" || { rm -f "$ovf"; die "recreate-candidate-safe fehlgeschlagen (FAIL CLOSED)"; }
rm -f "$ovf"
echo " Post-Gates:"
recreate_post_gates "$CANDIDATE_IMAGE_ID"
echo "RECREATE-CANDIDATE-SAFE OK. Container laeuft mit Candidate-Image."
}
# ---- rollback-to-recovery-gold (NUR historical-service; nur Red Queen) ----
rollback_to_recovery_gold() {
expect_zero "$@"
echo "=== ROLLBACK-TO-RECOVERY-GOLD ($RUN_MODE) ==="
echo " Ziel-Service : $HISTORICAL_SERVICE (NUR dieser; KEIN db, KEINE Volumes)"
echo " Image : $RECOVERY_GOLD_TAG_FINAL -> $RECOVERY_GOLD_IMAGE_ID (immutable Baseline)"
echo " Preconditions:"
echo " [pre] Recovery Image-ID inspectable:"
docker image inspect "$RECOVERY_GOLD_IMAGE_ID" >/dev/null 2>&1 || die "pre FAIL: Recovery-Image nicht inspectable (FAIL CLOSED)"
echo " OK"
echo " [pre] Recovery Tag korrekt:"
local rt; rt="$(docker image inspect -f '{{.Id}}' "$RECOVERY_GOLD_TAG_FINAL" 2>/dev/null || true)"
[ "$rt" = "$RECOVERY_GOLD_IMAGE_ID" ] || die "pre FAIL: Recovery-Tag != Recovery-ID (FAIL CLOSED)"
echo " OK"
echo " [pre] verify-recovery-gold-image PASS:"
( verify_recovery_gold_image >/dev/null 2>&1 ) && echo " OK" || die "pre FAIL: Recovery-Gold nicht verifiziert (FAIL CLOSED)"
echo " [pre] historical-db healthy:"
ensure_db_run && echo " OK"
if [ "$RUN_MODE" = "DRY_RUN" ]; then
echo "DRY_RUN_PASS: wuerde docker compose -f $COMPOSE_FILE -f <override> up -d --no-deps --no-build $HISTORICAL_SERVICE (Image=$RECOVERY_GOLD_TAG_FINAL). KEINE Aktion."
exit 0
fi
mode_or_die
local ovf; ovf="$(write_recreate_override "$RECOVERY_GOLD_TAG_FINAL")"
docker compose -f "$COMPOSE_FILE" -f "$ovf" up -d --no-deps --no-build "$HISTORICAL_SERVICE" || { rm -f "$ovf"; die "rollback-to-recovery-gold fehlgeschlagen (FAIL CLOSED)"; }
rm -f "$ovf"
echo " Post-Rollback:"
recreate_post_gates "$RECOVERY_GOLD_IMAGE_ID"
echo "ROLLBACK-TO-RECOVERY-GOLD OK. Container laeuft mit Recovery-Gold-Image."
}
# ---- build-historical (NUR Build, kein recreate; DRY_RUN = kein Bau) ---- # ---- build-historical (NUR Build, kein recreate; DRY_RUN = kein Bau) ----
build_historical() { build_historical() {
expect_zero "$@" expect_zero "$@"
@ -691,7 +921,7 @@ if [ "$SUBCMD" = "dry-run" ]; then
SUBCMD="${1:-}" SUBCMD="${1:-}"
shift || true shift || true
case "$SUBCMD" in case "$SUBCMD" in
build-historical|recreate-historical|snapshot-recovery|build-recovery-gold) ;; build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|pin-current-candidate|recreate-candidate-safe|rollback-to-recovery-gold) ;;
*) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold zulaessig" ;; *) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold zulaessig" ;;
esac esac
fi fi
@ -1060,6 +1290,10 @@ case "$SUBCMD" in
verify-recovery-snapshot) verify_recovery_snapshot "$@" ;; verify-recovery-snapshot) verify_recovery_snapshot "$@" ;;
build-recovery-gold) build_recovery_gold "$@" ;; build-recovery-gold) build_recovery_gold "$@" ;;
verify-recovery-gold-image) verify_recovery_gold_image "$@" ;; verify-recovery-gold-image) verify_recovery_gold_image "$@" ;;
pin-current-candidate) pin_current_candidate "$@" ;;
verify-candidate-image) verify_candidate_image "$@" ;;
recreate-candidate-safe) recreate_candidate_safe "$@" ;;
rollback-to-recovery-gold) rollback_to_recovery_gold "$@" ;;
build-historical) build_historical "$@" ;; build-historical) build_historical "$@" ;;
create-build-exclusion) expect_zero "$@" create-build-exclusion) expect_zero "$@"
# Build-Pollution-Exclusion implementieren (Punkt 5-8): .dockerignore anlegen/ersetzen # Build-Pollution-Exclusion implementieren (Punkt 5-8): .dockerignore anlegen/ersetzen
@ -1087,6 +1321,6 @@ case "$SUBCMD" in
recreate-historical) recreate_historical "$@" ;; recreate-historical) recreate_historical "$@" ;;
verify-gold-reproduction) verify_gold_reproduction "$@" ;; verify-gold-reproduction) verify_gold_reproduction "$@" ;;
*) die "Aufruf: ps|inspect|logs|run|code <K>|hashes|code-list|stage-dir|stage-check <K>|test <K>|search-writes|healthcheck|backup <K>|deploy <K>|rollback <K>|build-def|build-files|host-manifest|host-code <p>|container-manifest|container-code <p>|tree-diff|backup-sot|sot-session|recover-sot <K>|recover-sot-list|verify-sot-recovery|restore-sot|snapshot-recovery|dry-run snapshot-recovery|verify-recovery-snapshot|build-recovery-gold|dry-run build-recovery-gold|verify-recovery-gold-image|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion" *) die "Aufruf: ps|inspect|logs|run|code <K>|hashes|code-list|stage-dir|stage-check <K>|test <K>|search-writes|healthcheck|backup <K>|deploy <K>|rollback <K>|build-def|build-files|host-manifest|host-code <p>|container-manifest|container-code <p>|tree-diff|backup-sot|sot-session|recover-sot <K>|recover-sot-list|verify-sot-recovery|restore-sot|snapshot-recovery|dry-run snapshot-recovery|verify-recovery-snapshot|build-recovery-gold|dry-run build-recovery-gold|verify-recovery-gold-image|pin-current-candidate|dry-run pin-current-candidate|verify-candidate-image|recreate-candidate-safe|dry-run recreate-candidate-safe|rollback-to-recovery-gold|dry-run rollback-to-recovery-gold|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion"
;; ;;
esac esac