From c9b3dafd9f8ed4307169cbc07012ef91c303a0f1 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 29 Aug 2026 16:50:17 +0000 Subject: [PATCH] =?UTF-8?q?rq-historical:=20CONTROLLED=20NO-OP=20BUILD/REC?= =?UTF-8?q?REATE=20Kanal=20(build-historical,=20recreate-historical,=20ver?= =?UTF-8?q?ify-gold-reproduction)=20=E2=80=94=20dry-run=20Mutation=20Guard?= =?UTF-8?q?,=20.dockerignore=20Build-Pollution-Exclusion,=20fail-closed,?= =?UTF-8?q?=20kein=20echter=20Build/Recreate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../control-plane/cp2a2_2b/rq-historical | 213 +++++++++++++++++- 1 file changed, 212 insertions(+), 1 deletion(-) diff --git a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical index 89813e6..220d697 100644 --- a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical +++ b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical @@ -80,8 +80,33 @@ declare -A RECOVERY=( HOST_ONLY_LEGACY=( "/opt/historical-v2/app/app/backfill.py" "/opt/historical-v2/app/app/test_idempotency.py" + "/opt/historical-v2/app/eligibility.py" ) +# --------------------------------------------------------- BUILD/RECREATE-KANAL +# Phase 13.5 · Controlled No-op Build/Recreate Channel. +# Build-Pollution-Exclusion via .dockerignore (präzise, NUR Legacy-Artefakte). +# KEIN Businesscode-Fix. KEIN Backfill-Runner in diesem Kanal. +COMPOSE_PROJECT=/opt/historical-v2 +COMPOSE_FILE=/opt/historical-v2/docker-compose.yml +HISTORICAL_SERVICE=historical-service +GOLD_IMAGE_ID="sha256:f1a6f33e8714c5cdf017632aa30e71241a8ed670a5c95dac2e8226064b990c81" +GOLD_CONTAINER_ID="634cc505c7bc" # 634cc505c7bc (laufender Gold-Container) +GOLD_MANIFEST="/opt/rq-historical-gold-reference/gold_app_manifest.sha" +BUILD_EXCLUSION_FILE=/opt/historical-v2/.dockerignore +# Präzise Pollution-Exclusion-Einträge (NUR diese 3; keine Gold-Datei betroffen): +# app/app/ -> /app/app/app/ (verschachtelt, im Gold NICHT vorhanden) +# app/eligibility.py -> /app/app/eligibility.py (top-level, Gold: quality/eligibility.py UNBERÜHRT) +BUILD_EXCLUSION_LINES=( "app/app/" "app/eligibility.py" ) +# Legacy-Artefakte, die im neuen Container NICHT als EXTRA erscheinen dürfen: +declare -A FORBIDDEN_EXTRA=( + ["app/app/app/backfill.py"]=1 + ["app/app/app/test_idempotency.py"]=1 + ["app/app/eligibility.py"]=1 +) + +# RUN_MODE (Mutation Guard): READ_ONLY | DRY_RUN | MUTATING +RUN_MODE=MUTATING die() { echo "ERR: $*" >&2; exit 2; } req_ok() { @@ -95,6 +120,155 @@ key_ok() { } expect_zero() { [ $# -eq 0 ] || die "unerlaubtes Argument '$*'"; } +# ================= BUILD/RECREATE-KANAL HILFSFUNKTIONEN (alle fail-closed) === +# MODE ist global: MUTATING (Default) | DRY_RUN (keine Mutationen) +mode_or_die() { [ "$RUN_MODE" != "DRY_RUN" ] || die "DRY_RUN-Modus, aber mutierender Pfad erreicht (FAIL CLOSED)"; } + +sot_bestandscheck() { # gültiges vollständiges backup-sot vorhanden? + local latest bdir + latest="$(ls -1t "$SOT_BACKUP_ROOT" 2>/dev/null | head -1)" + [ -n "$latest" ] || die "kein backup-sot vorhanden (FAIL CLOSED)" + bdir="$SOT_BACKUP_ROOT/$latest" + [ -f "$bdir/historical-v2.tar" ] && [ -s "$bdir/SHA256.manifest" ] || die "backup-sot unvollständig (FAIL CLOSED)" + echo "$bdir" +} +verify_recovery_ok() { # 13/13 MATCH? + local allok=1 k csrc htarget gold hsha csha + for k in "${!RECOVERY[@]}"; do + IFS='|' read -r csrc htarget gold <<< "${RECOVERY[$k]}" + hsha="$(sha256sum "$htarget" 2>/dev/null | awk '{print $1}')" || hsha="" + csha="$(docker exec "$CONTAINER" sha256sum "$csrc" 2>/dev/null | awk '{print $1}')" || csha="" + if [ "$hsha" != "$gold" ] || [ "$csha" != "$gold" ]; then allok=0; echo " FEHLT: $k host=$hsha container=$csha"; fi + done + [ "$allok" = "1" ] || die "verify-sot-recovery NICHT 13/13 MATCH (FAIL CLOSED)" +} +ensure_gold_image() { docker image inspect "$GOLD_IMAGE_ID" >/dev/null 2>&1 || die "Gold-Image $GOLD_IMAGE_ID fehlt (FAIL CLOSED)"; } +ensure_gold_run() { docker inspect -f '{{.State.Running}}' "$HISTORICAL_SERVICE" >/dev/null 2>&1 || die "Gold-Container $HISTORICAL_SERVICE läuft nicht (FAIL CLOSED)"; } +ensure_db_run() { docker inspect -f '{{.State.Running}}' historical-db >/dev/null 2>&1 || die "historical-db läuft nicht (FAIL CLOSED)"; } +ensure_db_volume() { docker inspect -f '{{ range .Mounts }}{{ .Name }} {{ end }}' historical-db 2>/dev/null | grep -q 'historical-db-data' || die "DB-Volume historical-db-data fehlt (FAIL CLOSED)"; } +ensure_builddef() { [ -f "$COMPOSE_FILE" ] || die "Compose fehlt: $COMPOSE_FILE"; [ -f "$COMPOSE_PROJECT/Dockerfile" ] || die "Dockerfile fehlt"; } +exclusion_valid() { # .dockerignore exakt die 3 Legacy-Einträge, keine Gold-Datei ausgeschlossen + [ -f "$BUILD_EXCLUSION_FILE" ] || die ".dockerignore fehlt (FAIL CLOSED)" + local line + while IFS= read -r line; do + [ -z "$line" ] && continue + case "$line" in + app/app/|app/eligibility.py) ;; + *) die "unerwartete Exclusion '$line' im .dockerignore (FAIL CLOSED)" ;; + esac + done < "$BUILD_EXCLUSION_FILE" +} +# Manifest einer Quelle (Gold-Container / neuer Temp-Container) erzeugen: +# gibt zeilenweise 'sha app/' auf stdout; Quelle ist IMMER laufender Container +gen_manifest() { # $1 = docker-Container-Name + docker exec "$1" sh -c "cd /app && find app -type f -name '*.py' ! -path '*/__pycache__/*' 2>/dev/null | sort | while IFS= read -r f; do sha256sum \"\$f\" 2>/dev/null; done" +} +classify_and_report() { # $1 = neues Manifest Datei (Temp); vergleicht gegen GOLD_MANIFEST + FORBIDDEN_EXTRA + local new="$1" gold goldrel goldsha nrel nsha status + local d=0 m=0 x=0 mat=0 l=0 + # Gold-Dateien + while read -r goldsha goldrel; do + goldrel="${goldrel#./}" + if [ -n "$goldrel" ]; then + l=$((l+1)) + nsha="$(awk -v r="$goldrel" '$2==r{print $1}' "$new")" + if [ -z "$nsha" ]; then echo " MISSING $goldrel"; m=$((m+1)); + elif [ "$nsha" = "$goldsha" ]; then mat=$((mat+1)); + else echo " DIVERGED $goldrel (gold=$goldsha new=$nsha)"; d=$((d+1)); fi + fi + done < "$GOLD_MANIFEST" + # neue Dateien + while read -r nsha nrel; do + nrel="${nrel#./}" + found="$(awk -v r="$nrel" '$2==r{print $1}' "$GOLD_MANIFEST")" + if [ -z "$found" ]; then + echo " EXTRA $nrel" + if [ -n "${FORBIDDEN_EXTRA[$nrel]:-}" ]; then x=$((x+1)); FORBIDDEN_HIT=1; else x=$((x+1)); fi + fi + done < "$new" + echo "--- Gold $l Dateien: MATCH=$mat DIVERGED=$d MISSING=$m ; EXTRA=$x ---" + G_RET="ok"; [ $d -eq 0 ] && [ $m -eq 0 ] && [ $x -eq 0 ] || G_RET="fail" +} + +# ---- build-historical (NUR Build, kein recreate; DRY_RUN = kein Bau) ---- +build_historical() { + expect_zero "$@" + echo "=== BUILD-HISTORICAL ($RUN_MODE) ===" + echo " Projekt : $COMPOSE_PROJECT" + echo " Compose : $COMPOSE_FILE" + echo " Service : $HISTORICAL_SERVICE" + echo " Kontext : $COMPOSE_PROJECT (Dockerfile: $COMPOSE_PROJECT/Dockerfile)" + # 1. Dry-run zeigt Guards geplant, aber fuehrt sie (bis auf reine Checks) nicht aus + sot_bestandscheck >/dev/null + echo " [guard] backup-sot vorhanden: OK" + # verify-recovery check ist read-only, kann im dry-run laufen + echo " [guard] verify-sot-recovery:" + verify_recovery_ok + echo " [guard] Gold-Image $GOLD_IMAGE_ID: $(ensure_gold_image && echo OK)" + echo " [guard] Gold-Container Running: $(ensure_gold_run && echo OK)" + echo " [guard] historical-db Running: $(ensure_db_run && echo OK)" + echo " [guard] DB-Volume: $(ensure_db_volume && echo OK)" + echo " [guard] Builddefinition (Dockerfile+Compose): $(ensure_builddef && echo OK)" + echo " [guard] Build-Pollution-Exclusion valid:" + exclusion_valid && echo " .dockerignore OK (nur app/app/ + app/eligibility.py)" + if [ "$RUN_MODE" = "DRY_RUN" ]; then + echo "DRY_RUN_PASS: build-historical wuerde aus $COMPOSE_PROJECT bauen (keine Aktion)." + exit 0 + fi + mode_or_die + echo " [build] docker compose -f $COMPOSE_FILE build $HISTORICAL_SERVICE" + docker compose -f "$COMPOSE_FILE" build "$HISTORICAL_SERVICE" || die "build fehlgeschlagen" + echo " [result]" + docker images --filter reference="${HISTORICAL_SERVICE}" --format 'Image {{.ID}} Created {{.CreatedSince}}' + echo "BUILD-HISTORICAL OK. Gold-Image NICHT geloescht ($GOLD_IMAGE_ID). Nur Build, kein Recreate." +} + +# ---- recreate-historical (NUR historical-service, kein Runner; DRY_RUN = kein Recreate) ---- +recreate_historical() { + expect_zero "$@" + echo "=== RECREATE-HISTORICAL ($RUN_MODE) ===" + echo " Service : $HISTORICAL_SERVICE (NUR dieser; NICHT historical-db)" + sot_bestandscheck >/dev/null + echo " [guard] backup-sot vorhanden: OK" + echo " [guard] Gold-Image $GOLD_IMAGE_ID: $(ensure_gold_image && echo OK)" + echo " [guard] Gold-Manifest: $([ -f "$GOLD_MANIFEST" ] && echo OK || echo FEHLT)" + echo " [guard] historical-db Running: $(ensure_db_run && echo OK)" + echo " [guard] DB-Volume: $(ensure_db_volume && echo OK)" + if [ "$RUN_MODE" = "DRY_RUN" ]; then + echo "DRY_RUN_PASS: recreate-historical wuerde historical-service neu erstellen (keine Aktion)." + exit 0 + fi + mode_or_die + echo " [recreate] docker compose -f $COMPOSE_FILE up -d --no-deps $HISTORICAL_SERVICE" + docker compose -f "$COMPOSE_FILE" up -d --no-deps "$HISTORICAL_SERVICE" || die "recreate fehlgeschlagen" + sleep 4 + local running + running="$(docker inspect -f '{{.State.Running}}' "$HISTORICAL_SERVICE" 2>/dev/null)" + [ "$running" = "true" ] || die "neuer Container laeuft nicht (FAIL CLOSED)" + echo " [Container] ID: $(docker inspect -f '{{.ID}}' "$HISTORICAL_SERVICE")" + echo " [Image] $(docker inspect -f '{{.Image}}' "$HISTORICAL_SERVICE")" + echo "RECREATE-HISTORICAL OK. KEIN Runner, KEIN compose down, KEIN Volume-Remove." +} + +# ---- verify-gold-reproduction (read-only) ---- +verify_gold_reproduction() { + req_ok; expect_zero "$@" + echo "=== VERIFY-GOLD-REPRODUCTION (Gold-Manifest vs Running-Container) ===" + [ -f "$GOLD_MANIFEST" ] || die "Gold-Manifest fehlt (FAIL CLOSED)" + local tmp; tmp="$(mktemp)" + gen_manifest "$HISTORICAL_SERVICE" > "$tmp" + classify_and_report "$tmp" + local ret="$G_RET" + rm -f "$tmp" + if [ "$ret" = "ok" ]; then + echo "VERDICT: GOLD REPRODUCTION OK (0 DIVERGED/MISSING/unerlaubte EXTRA)" + exit 0 + else + echo "VERDICT: GOLD REPRODUCTION FAIL" + exit 2 + fi +} + validate_stage() { local key="$1" file real owner dmode file="$(stage_file "$key")" @@ -120,6 +294,17 @@ redact_line() { SUBCMD="${1:-}" shift || true +# Mutation Guard: "dry-run " setzt DRY_RUN-Modus, dann normaler Dispatch. +if [ "$SUBCMD" = "dry-run" ]; then + RUN_MODE=DRY_RUN + SUBCMD="${1:-}" + shift || true + case "$SUBCMD" in + build-historical|recreate-historical) ;; + *) die "dry-run nur mit build-historical|recreate-historical zulaessig" ;; + esac +fi + case "$SUBCMD" in # ---------------- bestehend ---------------- ps) expect_zero "$@"; exec docker ps ;; @@ -480,7 +665,33 @@ case "$SUBCMD" in fi echo "RESTORE-SOT OK. KEIN Build/Recreate danach." ;; + build-historical) build_historical "$@" ;; + create-build-exclusion) expect_zero "$@" + # Build-Pollution-Exclusion implementieren (Punkt 5-8): .dockerignore anlegen/ersetzen + # PREVIOUS_STATE war ABSENT. NUR exakt die 3 Legacy-Einträge. + if [ -f "$BUILD_EXCLUSION_FILE" ]; then + die "create-build-exclusion: .dockerignore existiert bereits (kein Ueberschreiben ohne Backup — manuell)" + fi + printf 'app/app/\napp/eligibility.py\n' > "$BUILD_EXCLUSION_FILE" + echo "BUILD-EXCLUSION OK: $BUILD_EXCLUSION_FILE" + exclusion_valid && { echo " validiert: nur app/app/ + app/eligibility.py"; echo " (PREVIOUS_STATE=ABSENT; keine Gold-Datei betroffen)"; } + ;; + show-build-exclusion) expect_zero "$@" + if [ ! -f "$BUILD_EXCLUSION_FILE" ]; then echo "PREVIOUS_STATE=ABSENT"; exit 0; fi + echo "=== .dockerignore Inhalt ===" + cat "$BUILD_EXCLUSION_FILE" + echo "=== build-exclusion: Gold-Dateien betroffen? (sollte: 0) ===" + ok=0 + while IFS= read -r f; do + # f relativ zu /opt/historical-v2; pruefe ob im Gold-Manifest (app/...) + rel="app/${f#app/}" + if grep -q " $rel$" "$GOLD_MANIFEST" 2>/dev/null; then echo " GOLD-DATEI BETROFFEN: $rel"; ok=1; fi + done < "$BUILD_EXCLUSION_FILE" + [ "$ok" = "0" ] && echo " 0 Gold-Dateien ausgeschlossen (OK)" + ;; + recreate-historical) recreate_historical "$@" ;; + verify-gold-reproduction) verify_gold_reproduction "$@" ;; - *) die "Aufruf: ps|inspect|logs|run|code |hashes|code-list|stage-dir|stage-check |test |search-writes|healthcheck|backup |deploy |rollback |build-def|build-files|host-manifest|host-code

|container-manifest|container-code

|tree-diff|backup-sot|sot-session|recover-sot |recover-sot-list|verify-sot-recovery|restore-sot" + *) die "Aufruf: ps|inspect|logs|run|code |hashes|code-list|stage-dir|stage-check |test |search-writes|healthcheck|backup |deploy |rollback |build-def|build-files|host-manifest|host-code

|container-manifest|container-code

|tree-diff|backup-sot|sot-session|recover-sot |recover-sot-list|verify-sot-recovery|restore-sot|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion" ;; esac