rq-historical: CONTROLLED NO-OP BUILD/RECREATE Kanal (build-historical, recreate-historical, verify-gold-reproduction) — dry-run Mutation Guard, .dockerignore Build-Pollution-Exclusion, fail-closed, kein echter Build/Recreate
This commit is contained in:
parent
64240ea3b1
commit
c9b3dafd9f
1 changed files with 212 additions and 1 deletions
|
|
@ -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/<rel>' 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 <subcmd>" 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 <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"
|
||||
*) 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|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion"
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
Loading…
Reference in a new issue