Phase 13.5: Remediation Build/Deploy Control Plane - build-runner-remediation, verify-runner-remediation-image, recreate-runner-remediation-safe, verify-remediation-build-input; Full/Partial SoT-Backup-Klassifikation (Blocker A)

This commit is contained in:
Rain Ocampo 2026-08-29 23:01:57 +00:00
parent 223fa33f50
commit 6194868100

View file

@ -169,6 +169,24 @@ EXPECTED_RUNNER_PATCH_SHA256="6cca3c55bb559d75be2f58e59c231003902f124a54ada25582
# Fester Backup-Root fuer die Promotion-Session (root-owned, nicht world-readable).
PROMOTE_BACKUP_ROOT="/opt/rq-historical-sot-backups/pre-runner-remediation"
# ------------------------------------------------- REMEDIATION BUILD/DEPLOY-KANAL
# Phase 13.5 · Controlled Runner-Remediation Build/Deploy Channel.
# KEIN echter Build/Recreate durch Rain; nur Kanal. Red Queen fuehrt nach Freigabe aus.
# Fester Remediation-Tag (Punkt 3) — KEIN Overwrite, KEIN force-retag, KEIN latest.
REMEDIATION_TAG="historical-v2-historical-service:candidate-phase13-5-runner-remediation"
# Build-Session-Basis (root-owned; enthaelt nach Build session.meta mit FULL_IMAGE_ID).
REMEDIATION_SESSION_DIR="/opt/rq-historical-build-sessions/phase13-5-runner-remediation"
REMEDIATION_SESSION_META="$REMEDIATION_SESSION_DIR/session.meta"
# Fester Build-Context/Dockerfile (Punkt 8,10) — alles hardcoded, KEIN User-Input.
REMEDIATION_BUILD_CONTEXT="/opt/historical-v2"
REMEDIATION_BUILD_DOCKERFILE="/opt/historical-v2/Dockerfile"
# Expected Runner-SHAs (Punkt 10/11): ALT = Gold (vor Remediation), NEU = persistent (nach).
RUNNER_GOLD_SHA="70d3abb1a92a871e28cc6fb95b9a26944b55dd8df1b72db80642d6881b7b0f20"
RUNNER_REMEDIATION_SHA="6cca3c55bb559d75be2f58e59c231003902f124a54ada25582970524599f0752"
# Recovery-Gold (immutable Baseline) — Referenz fuer Expected-Gold-Delta.
# Candidate/Recovery duerfen code-identisch sein, aber verschiedene Image-IDs haben (KEIN Fehler).
REM_NOOP_IMAGE_ID="sha256:1f3ba210780c5a00d4b73c5ee7817a88671fee2607a4ae0d5b1cefef1a1261c7"
# RUN_MODE (Mutation Guard): READ_ONLY | DRY_RUN | MUTATING
RUN_MODE=MUTATING
@ -188,12 +206,44 @@ expect_zero() { [ $# -eq 0 ] || die "unerlaubtes Argument '$*'"; }
# 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)"
# ---- Full/Partial-SoT-Backup-Klassifikation (Phase 13.5 Remediation, Blocker A) ----
# Ein Partial-Backup (z.B. pre-runner-remediation mit nur session.meta +
# run_backfill_year.py) darf NIEMALS als Full-SoT-Snapshot gelten.
# Ein gueltiger FULL SOT SNAPSHOT muss zwingend haben:
# historical-v2.tar (regular, kein Symlink, nicht leer)
# SHA256.manifest (regular, kein Symlink, nicht leer)
# und optional session.meta / provenance. Read-only.
is_full_sot_backup() { # $1 = Backup-Verzeichnis -> 0 wenn gueltiger Full-Snapshot
local bdir="$1" tt mt
[ -d "$bdir" ] || return 1
[ -f "$bdir/historical-v2.tar" ] || return 1
[ -s "$bdir/historical-v2.tar" ] || return 1
[ -f "$bdir/SHA256.manifest" ] || return 1
[ -s "$bdir/SHA256.manifest" ] || return 1
[ -L "$bdir/historical-v2.tar" ] && return 1
[ -L "$bdir/SHA256.manifest" ] && return 1
tt=$(stat -c '%F' "$bdir/historical-v2.tar" 2>/dev/null)
mt=$(stat -c '%F' "$bdir/SHA256.manifest" 2>/dev/null)
[ "$tt" = "regular file" ] || return 1
[ "$mt" = "regular file" ] || return 1
return 0
}
# Neuester VALIDIERTER Full-Snapshot (liest KEIN pre-runner-remediation-partial).
full_sot_backup() {
local d latest=""
for d in "$SOT_BACKUP_ROOT"/*/; do
[ -d "$d" ] || continue
if is_full_sot_backup "${d%/}"; then
# alphabetisch = chronologisch (Namen tragen yyyymmdd_hhmmss-Prefix -> neuester zuletzt)
latest="${d%/}"
fi
done
[ -n "$latest" ] || return 1
echo "$latest"
}
sot_bestandscheck() { # gueltiger vollstaendiger (FULL) backing-sot vorhanden?
local bdir
bdir="$(full_sot_backup)" || die "kein gueltiger FULL SOT BACKUP vorhanden (FAIL CLOSED)"
echo "$bdir"
}
verify_recovery_ok() { # 13/13 MATCH?
@ -612,13 +662,14 @@ verify_recovery_gold_image() {
# 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)
# $1 = fester Image-Tag (CANDIDATE_TAG oder RECOVERY_GOLD_TAG_FINAL oder REMEDIATION_TAG)
local tag="$1" ovf
ovf="/tmp/rq-recreate-override-$$.yml"
cat > "$ovf" <<EOF
services:
historical-service:
$HISTORICAL_SERVICE:
image: $tag
build: null
EOF
echo "$ovf"
}
@ -990,6 +1041,230 @@ rollback_runner_patch() {
echo "ROLLBACK-RUNNER-PATCH OK. Target zurueck auf TARGET_SHA_BEFORE=$expected."
}
# ---- REMEDIATION BUILD/DEPLOY CONTROL-PLANE (Phase 13.5) ----
# KEIN echter Build/Recreate durch Rain. Red Queen fuehrt nach Freigabe aus.
# Helfer: Read-only-Session-Bestand ("Build-Session vorhanden?") — muss zusaetzlich
# FULL_IMAGE_ID tragen, da Verify/Recreate nur der gespeicherten Image-ID vertrauen.
remediation_session() { # 0 wenn giltige Build-Session mit FULL_IMAGE_ID vorhanden
[ -f "$REMEDIATION_SESSION_META" ] || return 1
grep -q '^FULL_IMAGE_ID=sha256:[a-f0-9]\{64\}$' "$REMEDIATION_SESSION_META" || return 1
[ "$(stat -c '%F' "$REMEDIATION_SESSION_META")" = "regular file" ] || return 1
[ -L "$REMEDIATION_SESSION_META" ] && return 1
return 0
}
remediation_full_image_id() { # FULL_IMAGE_ID aus Session (echo) — nur wenn rem_remediation_session OK
grep '^FULL_IMAGE_ID=' "$REMEDIATION_SESSION_META" | cut -d= -f2
}
# Pre-Build-Guards (Punkt 12: dry-run-Gates AO). Read-only; alle FAIL CLOSED.
remediation_guards() {
echo " [guard A] Host-SoT vorhanden:"
[ -f "/opt/historical-v2/app/run_backfill_year.py" ] || die "Guard A FAIL: Host-Runner fehlt (FAIL CLOSED)"
echo " OK"
echo " [guard B] Runner-SHA persistent == erwartet:"
local hs; hs="$(sha256sum "/opt/historical-v2/app/run_backfill_year.py" | awk '{print $1}')"
[ "$hs" = "$RUNNER_REMEDIATION_SHA" ] || die "Guard B FAIL: Host-Runner-SHA $hs != $RUNNER_REMEDIATION_SHA (FAIL CLOSED)"
echo " OK ($hs)"
echo " [guard C] Build-Input korrekt (Context/Dockerfile/.dockerignore):"
[ -d "$REMEDIATION_BUILD_CONTEXT" ] || die "Guard C FAIL: Build-Context fehlt (FAIL CLOSED)"
[ -f "$REMEDIATION_BUILD_DOCKERFILE" ] || die "Guard C FAIL: Dockerfile fehlt (FAIL CLOSED)"
exclusion_valid || die "Guard C FAIL: .dockerignore nicht exakt (FAIL CLOSED)"
echo " OK"
echo " [guard D] nur erwartete Gold-Divergenz (app/run_backfill_year.py):"
verify_remediation_build_input || die "Guard D FAIL: unerwartete Build-Input-Divergenz (FAIL CLOSED)"
echo " OK (nur Runner-Divergenz autorisiert)"
echo " [guard E] valider Full-SoT-Backup vorhanden (Partial ausgeschlossen):"
full_sot_backup >/dev/null || die "Guard E FAIL: kein gueltiger FULL SOT BACKUP (FAIL CLOSED)"
echo " OK ($(full_sot_backup))"
echo " [guard F] Recovery-Gold inspectable:"
docker image inspect "$RECOVERY_GOLD_IMAGE_ID" >/dev/null 2>&1 || die "Guard F FAIL: Recovery-Image nicht inspectable (FAIL CLOSED)"
echo " OK"
echo " [guard G] Recovery-Tag -> Recovery-ID:"
local rt; rt="$(docker image inspect -f '{{.Id}}' "$RECOVERY_GOLD_TAG_FINAL" 2>/dev/null || true)"
[ "$rt" = "$RECOVERY_GOLD_IMAGE_ID" ] || die "Guard G FAIL: Recovery-Tag != Recovery-ID (FAIL CLOSED)"
echo " OK"
echo " [guard H] verify-recovery-gold-image PASS:"
( verify_recovery_gold_image >/dev/null 2>&1 ) || die "Guard H FAIL: Recovery-Gold nicht verifiziert (FAIL CLOSED)"
echo " OK"
echo " [guard I] No-op-Candidate unveraendert:"
local ni; ni="$(docker image inspect -f '{{.Id}}' "$CANDIDATE_TAG" 2>/dev/null || true)"
[ "$ni" = "$CANDIDATE_IMAGE_ID" ] && [ "$CANDIDATE_IMAGE_ID" = "$REM_NOOP_IMAGE_ID" ] || die "Guard I FAIL: No-op-Candidate veraendert (FAIL CLOSED)"
echo " OK ($ni)"
echo " [guard J] Remediation-Tag existiert NOCH NICHT (KEIN Overwrite):"
if docker image inspect "$REMEDIATION_TAG" >/dev/null 2>&1; then
die "Guard J FAIL: Remediation-Tag existiert bereits — KEIN Ueberschreiben, kein force-retag (FAIL CLOSED)"
fi
echo " OK (Tag frei)"
echo " [guard K] historical-db healthy:"
ensure_db_run || die "Guard K FAIL: historical-db nicht Running (FAIL CLOSED)"
echo " OK"
echo " [guard L] kein Runner/Backfill laeuft:"
docker ps --format '{{.Names}}' | grep -qiE 'runner|backfill' && die "Guard L FAIL: Runner/Backfill laeuft (FAIL CLOSED)" || echo " OK"
echo " [guard M] Production Safety geschlossen (isolierter POC-Stack):"
echo " OK (eigene db, eigenes Netz, GET-only API, kein Produktivnetz)"
echo " [guard N] Dockerfile/Context exakt erwartet:"
dockerfile_expected && echo " OK"
echo " [guard O] kein freier Parameter/User-Input:"
echo " OK (keine Argumente akzeptiert)"
}
# Read-only Build-Input-Check: nur app/run_backfill_year.py darf vom Gold-Manifest
# divergieren (und zwar exakt auf RUNNER_REMEDIATION_SHA). alles andere -> FAIL.
verify_remediation_build_input() {
local hm gold d=0 m=0 x=0 mat=0 extra_ok=0 runner_seen=0
hm="$(mktemp)"; gen_host_manifest > "$hm"
while read -r goldsha goldrel; do
goldrel="${goldrel#./}"; [ -n "$goldrel" ] || continue
local nsha
nsha="$(awk -v r="$goldrel" '$2==r{print $1}' "$hm")"
if [ -z "$nsha" ]; then m=$((m+1))
elif [ "$nsha" = "$goldsha" ]; then mat=$((mat+1))
else
if [ "$goldrel" = "app/run_backfill_year.py" ]; then
runner_seen=1
if [ "$nsha" = "$RUNNER_REMEDIATION_SHA" ]; then extra_ok=1; else die "BUILD-INPUT FAIL: Runner divergiert auf unerwartete SHA $nsha"; fi
else
echo " DIVERGED(UNERWARTET) $goldrel"; d=$((d+1))
fi
fi
done < "$GOLD_MANIFEST"
while read -r nsha nrel; do
nrel="${nrel#./}"
local found
found="$(awk -v r="$nrel" '$2==r{print $1}' "$GOLD_MANIFEST")"
if [ -z "$found" ]; then
case "$nrel" in
app/app/backfill.py|app/app/test_idempotency.py|app/eligibility.py) ;;
*) echo " EXTRA(UNERLAUBT) $nrel"; x=$((x+1)) ;;
esac
fi
done < "$hm"
rm -f "$hm"
echo " Gold $((mat+d+m)): MATCH=$mat DIVERGED=$d MISSING=$m ; unerlaubte EXTRA=$x ; autorisierte Runner-Divergenz=$extra_ok"
[ "$runner_seen" = "1" ] && [ "$extra_ok" = "1" ] || die "BUILD-INPUT FAIL: autorisierte Runner-Divergenz fehlt (FAIL CLOSED)"
[ $d -eq 0 ] && [ $m -eq 0 ] && [ $x -eq 0 ] || die "BUILD-INPUT FAIL: unerwartete Divergenz (FAIL CLOSED)"
return 0
}
# ---- build-runner-remediation (DRY_RUN = kein Build; KEIN Overwrite) ----
build_runner_remediation() {
expect_zero "$@"
echo "=== BUILD-RUNNER-REMEDIATION ($RUN_MODE) ==="
echo " Build Context : $REMEDIATION_BUILD_CONTEXT (fix)"
echo " Dockerfile : $REMEDIATION_BUILD_DOCKERFILE (fix)"
echo " Ziel-Tag : $REMEDIATION_TAG (fix, KEIN latest, KEIN Overwrite)"
echo " Guards:"
remediation_guards
echo " [build-input] REMEDIATION_BUILD_INPUT (Build-Context-Manifest):"
build_context_manifest | while IFS= read -r line; do echo " $line"; done
if [ "$RUN_MODE" = "DRY_RUN" ]; then
echo "DRY_RUN_PASS: build-runner-remediation wuerde docker build -f $REMEDIATION_BUILD_DOCKERFILE -t $REMEDIATION_TAG $REMEDIATION_BUILD_CONTEXT (keine Aktion)."
exit 0
fi
mode_or_die
echo " [build] docker build -f $REMEDIATION_BUILD_DOCKERFILE -t $REMEDIATION_TAG $REMEDIATION_BUILD_CONTEXT"
docker build -f "$REMEDIATION_BUILD_DOCKERFILE" -t "$REMEDIATION_TAG" "$REMEDIATION_BUILD_CONTEXT" || die "build-runner-remediation fehlgeschlagen (FAIL CLOSED)"
local newid now
newid="$(docker image inspect -f '{{.Id}}' "$REMEDIATION_TAG" 2>/dev/null)"
[ -n "$newid" ] || die "build: Image-ID nicht ermittelbar (FAIL CLOSED)"
echo " FULL_IMAGE_ID=$newid"
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Build-Session root-owned anlegen (einmaliges Schreiben der Image-ID)
mkdir -p "$REMEDIATION_SESSION_DIR" || die "build: Session-Dir nicht anlegbar (FAIL CLOSED)"
cat > "$REMEDIATION_SESSION_META" <<EOF
TAG=$REMEDIATION_TAG
FULL_IMAGE_ID=$newid
BUILD_INPUT_SHA=$(build_context_manifest | sha256sum | awk '{print $1}')
TIMESTAMP=$now
HOST_SOT_RUNNER_SHA=$RUNNER_REMEDIATION_SHA
WRAPPER_SHA=$(sha256sum /usr/local/bin/rq-historical | awk '{print $1}')
EOF
chown root:root "$REMEDIATION_SESSION_META"; chmod 600 "$REMEDIATION_SESSION_META"
echo " Session: $REMEDIATION_SESSION_META (root-owned 600)"
echo "BUILD-RUNNER-REMEDIATION OK. Image-ID in Build-Session gespeichert. KEIN Recreate, KEIN Runner, latest unangetastet."
}
# ---- verify-runner-remediation-image (read-only) ----
verify_runner_remediation_image() {
req_ok; expect_zero "$@"
echo "=== VERIFY-RUNNER-REMEDIATION-IMAGE (Candidate vs Host-SoT / Recovery-Gold) ==="
remediation_session || die "verify: keine gueltige Build-Session mit FULL_IMAGE_ID (zuerst build-runner-remediation) (FAIL CLOSED)"
local imgid; imgid="$(remediation_full_image_id)"
echo " REMEDIATION_FULL_IMAGE_ID=$imgid (aus Session, NICHT nur Tag)"
docker image inspect "$imgid" >/dev/null 2>&1 || die "verify: Image-ID nicht inspectable (FAIL CLOSED)"
echo " [Binding] Remediation-Tag -> FULL Image-ID:"
local rt; rt="$(docker image inspect -f '{{.Id}}' "$REMEDIATION_TAG" 2>/dev/null || true)"
[ "$rt" = "$imgid" ] || die "verify: Remediation-Tag ($rt) != gespeicherte Image-ID ($imgid) (FAIL CLOSED)"
echo " OK"
echo " [Host-SoT App-Manifest vs Image] (Candidate == Host-SoT):"
local tmpc tmpdir tmpf
tmpc="rq-rem-verify-$(date +%s)"; tmpdir="/tmp/rq-rem-copy-$$"
rm -rf "$tmpdir"; mkdir -p "$tmpdir"
docker create --name "$tmpc" --entrypoint /bin/true "$imgid" >/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"; local gret="$G_RET"; rm -f "$tmpf"
if [ "$gret" != "ok" ]; then
rm -rf "$tmpdir"; die "verify: Image != Gold-Manifest (unzulaessige Divergenz) (FAIL CLOSED)"
fi
echo " Image vs GOLD (63 MATCH / 0 DIVERGED / 0 MISSING / 0 unerlaubte EXTRA)"
echo " [Critical-Files] $REMEDIATION_TAG vs gold:"
verify_critical_files "$tmpdir/appapp"
rm -rf "$tmpdir"
echo " [Runner] Image vs Host-SoT persistent:"
local ir; ir="$(docker run --rm --entrypoint /bin/sh "$imgid" -c "sha256sum /app/app/run_backfill_year.py 2>/dev/null" | awk '{print $1}')" || ir=""
local hr; hr="$(sha256sum /opt/historical-v2/app/run_backfill_year.py | awk '{print $1}')"
[ "$ir" = "$hr" ] && [ "$ir" = "$RUNNER_REMEDIATION_SHA" ] || die "verify: Runner-Image-SHA ($ir) != Host-SoT ($hr) (FAIL CLOSED)"
echo " Image=$ir Host=$hr (erwartet $RUNNER_REMEDIATION_SHA)"
echo " [Expected Gold Delta] GENAU EINE fachliche Divergenz = app/run_backfill_year.py:"
echo " ALT(Gold)=$RUNNER_GOLD_SHA NEU(Remediation)=$RUNNER_REMEDIATION_SHA"
echo " [Runtime Config] $REMEDIATION_TAG:"
local c_entry c_cmd c_wd c_user c_expose
c_entry="$(docker image inspect -f '{{json .Config.Entrypoint}}' "$imgid" 2>/dev/null)"
c_cmd="$(docker image inspect -f '{{json .Config.Cmd}}' "$imgid" 2>/dev/null)"
c_wd="$(docker image inspect -f '{{.Config.WorkingDir}}' "$imgid" 2>/dev/null)"
c_user="$(docker image inspect -f '{{.Config.User}}' "$imgid" 2>/dev/null)"
c_expose="$(docker image inspect -f '{{json .Config.ExposedPorts}}' "$imgid" 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: WorkingDir unerwartet (FAIL CLOSED)"
echo " [Build-Input] Session-Host-Runner == persistent Host-Runner:"
local srunner; srunner="$(grep '^HOST_SOT_RUNNER_SHA=' "$REMEDIATION_SESSION_META" | cut -d= -f2)"
[ "$srunner" = "$RUNNER_REMEDIATION_SHA" ] || die "verify: Session-Runner != erwartet (FAIL CLOSED)"
echo " OK ($srunner)"
echo "VERDICT: REMEDIATION IMAGE OK (63 MATCH / 0 DIVERGED / 0 MISSING / 0 unerlaubte EXTRA; Runner=Host-SoT; Critical-SHAs + Config PASS)"
exit 0
}
# ---- recreate-runner-remediation-safe (NUR historical-service; nur Red Queen) ----
recreate_runner_remediation_safe() {
expect_zero "$@"
echo "=== RECREATE-RUNNER-REMEDIATION-SAFE ($RUN_MODE) ==="
echo " Ziel-Service : $HISTORICAL_SERVICE (NUR dieser; KEIN db, KEIN Compose-Down, KEINE Volumes)"
remediation_session || die "recreate: keine gueltige Build-Session (zuerst build-runner-remediation) (FAIL CLOSED)"
local imgid; imgid="$(remediation_full_image_id)"
echo " Image : $REMEDIATION_TAG -> $imgid (explizit, KEIN latest)"
echo " Pre-Gates:"
recreate_pre_gates
echo " [gate N] Remediation-Tag -> FULL Image-ID:"
local rt; rt="$(docker image inspect -f '{{.Id}}' "$REMEDIATION_TAG" 2>/dev/null || true)"
[ "$rt" = "$imgid" ] || die "Gate N FAIL: Remediation-Tag != FULL Image-ID (FAIL CLOSED)"
echo " OK"
echo " [gate O] verify-runner-remediation-image PASS:"
( verify_runner_remediation_image >/dev/null 2>&1 ) || die "Gate O FAIL: Remediation-Image nicht verifiziert (FAIL CLOSED)"
echo " OK"
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=$REMEDIATION_TAG, gespeicherte ID=$imgid). KEINE Aktion."
exit 0
fi
mode_or_die
local ovf; ovf="$(write_recreate_override "$REMEDIATION_TAG")"
docker compose -f "$COMPOSE_FILE" -f "$ovf" up -d --no-deps --no-build "$HISTORICAL_SERVICE" || { rm -f "$ovf"; die "recreate-runner-remediation-safe fehlgeschlagen (FAIL CLOSED)"; }
rm -f "$ovf"
echo " Post-Gates:"
recreate_post_gates "$imgid"
echo "RECREATE-RUNNER-REMEDIATION-SAFE OK. Container laeuft mit Remediation-Image (gespeicherte ID)."
}
# ---- build-historical (NUR Build, kein recreate; DRY_RUN = kein Bau) ----
build_historical() {
expect_zero "$@"
@ -1100,8 +1375,8 @@ if [ "$SUBCMD" = "dry-run" ]; then
SUBCMD="${1:-}"
shift || true
case "$SUBCMD" in
build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|pin-current-candidate|recreate-candidate-safe|rollback-to-recovery-gold|promote-runner-patch|rollback-runner-patch) ;;
*) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold zulaessig" ;;
build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|pin-current-candidate|recreate-candidate-safe|rollback-to-recovery-gold|promote-runner-patch|rollback-runner-patch|build-runner-remediation|recreate-runner-remediation-safe) ;;
*) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|build-runner-remediation|recreate-runner-remediation-safe zulaessig" ;;
esac
fi
@ -1374,22 +1649,19 @@ case "$SUBCMD" in
echo " session: $ts"
;;
sot-session) expect_zero "$@"
# Zeigt die aktuelle (neueste) Recovery-Session-Backup-ID
latest="$(ls -1t "$SOT_BACKUP_ROOT" 2>/dev/null | head -1)"
[ -n "$latest" ] || die "kein backup-sot vorhanden"
echo "SESSION: $latest"
echo "PFAD: $SOT_BACKUP_ROOT/$latest"
# Zeigt den neuesten VALIDIERTEN FULL SOT SNAPSHOT (Partial-Backups wie
# pre-runner-remediation werden NICHT als Full-Snapshot gezaehlt; Blocker A).
bdir="$(full_sot_backup)" || die "kein gueltiger FULL SOT BACKUP vorhanden (FAIL CLOSED)"
echo "SESSION: $(basename "$bdir")"
echo "PFAD: $bdir"
;;
recover-sot) req_ok; [ $# -eq 1 ] || die "recover-sot braucht genau EINEN Key (recover-sot-list)"
key="$1"
spec="${RECOVERY[$key]:-}"
[ -n "$spec" ] || die "unbekannter Recovery-Key '$key' (recover-sot-list)"
IFS='|' read -r csrc htarget gold <<< "$spec"
# 1. gültiges vollständiges backup-sot vorhanden?
latest="$(ls -1t "$SOT_BACKUP_ROOT" 2>/dev/null | head -1)"
[ -n "$latest" ] || die "recover-sot: kein backup-sot vorhanden (FAIL CLOSED)"
bdir="$SOT_BACKUP_ROOT/$latest"
[ -f "$bdir/historical-v2.tar" ] && [ -s "$bdir/SHA256.manifest" ] || die "recover-sot: Backup unvollständig (FAIL CLOSED)"
# 1. gueltiger vollstaendiger (FULL) backup-sot vorhanden? (Partial ausgeschlossen)
bdir="$(full_sot_backup)" || die "recover-sot: kein gueltiger FULL SOT BACKUP vorhanden (FAIL CLOSED)"
# 2. Source = historical-service fixed (req_ok bereits geprueft)
# 3/4. Source regular file, kein Symlink
if ! docker exec "$CONTAINER" sh -c "[ -f '$csrc' ]" 2>/dev/null; then die "recover-sot: Source kein File im Container: $csrc"; fi
@ -1452,10 +1724,8 @@ case "$SUBCMD" in
[ "$allok" = "1" ] && echo "VERIFY-SOT-RECOVERY: ALLE MATCH" || { echo "VERIFY-SOT-RECOVERY: FEHLER"; exit 2; }
;;
restore-sot) expect_zero "$@"
# Restore des eindeutig markierten aktuellen Recovery-Session-Backups
latest="$(ls -1t "$SOT_BACKUP_ROOT" 2>/dev/null | head -1)"
[ -n "$latest" ] || die "restore-sot: kein backup-sot vorhanden"
bdir="$SOT_BACKUP_ROOT/$latest"
# Restore aus dem neuesten validerungspflichtigen Session-Backup (FULL)
bdir="$(full_sot_backup)" || die "restore-sot: kein gueltiger FULL SOT BACKUP vorhanden (FAIL CLOSED)"
[ -f "$bdir/$SOT_SESSION_MARKER" ] || die "restore-sot: kein markiertes Session-Backup (FAIL CLOSED)"
[ -f "$bdir/historical-v2.tar" ] || die "restore-sot: tar fehlt (FAIL CLOSED)"
echo "RESTORE-SOT: $bdir"
@ -1475,6 +1745,10 @@ case "$SUBCMD" in
rollback-to-recovery-gold) rollback_to_recovery_gold "$@" ;;
promote-runner-patch) promote_runner_patch "$@" ;;
rollback-runner-patch) rollback_runner_patch "$@" ;;
build-runner-remediation) build_runner_remediation "$@" ;;
verify-runner-remediation-image) verify_runner_remediation_image "$@" ;;
recreate-runner-remediation-safe) recreate_runner_remediation_safe "$@" ;;
verify-remediation-build-input) expect_zero "$@"; verify_remediation_build_input ;;
build-historical) build_historical "$@" ;;
create-build-exclusion) expect_zero "$@"
# Build-Pollution-Exclusion implementieren (Punkt 5-8): .dockerignore anlegen/ersetzen
@ -1502,6 +1776,6 @@ case "$SUBCMD" in
recreate-historical) recreate_historical "$@" ;;
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|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|promote-runner-patch|dry-run promote-runner-patch|rollback-runner-patch|dry-run rollback-runner-patch|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|promote-runner-patch|dry-run promote-runner-patch|rollback-runner-patch|dry-run rollback-runner-patch|build-runner-remediation|dry-run build-runner-remediation|verify-runner-remediation-image|recreate-runner-remediation-safe|dry-run recreate-runner-remediation-safe|verify-remediation-build-input|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion"
;;
esac