Phase 13.5 Remediation Build-Context Control-Plane Rotation: build-runner-remediation von /opt/historical-v2 auf immutable Gold-Source-Session 20260830_104127 + autorisierter Runner-Overlay 6cca3c55; Guards A/B/C/D/N auf Gold+Overlay; Guard I diagnostisch (No-op-Tag-Loss NON-BLOCKING); verify_runner_remediation_image 62+1 + repository.py Hard Gate; verify_archive/pin/archive aktive Build-Session-Identity; deterministischer REMEDIATION_BUILD_INPUT_SHA
This commit is contained in:
parent
8161208f1c
commit
c95fbe6b3c
1 changed files with 253 additions and 59 deletions
|
|
@ -237,9 +237,18 @@ REMEDIATION_TAG="historical-v2-historical-service:candidate-phase13-5-runner-rem
|
|||
# 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"
|
||||
# Fester Build-Context/Dockerfile (Punkt 8,10) — NICHT mehr statisch auf /opt/historical-v2.
|
||||
# Wird zur Laufzeit aus der kontrollierten Gold-Source-Session (RECOVERY_SOURCE_STATE)
|
||||
# + autorisiertem Runner-Overlay als kontrollierter Staging-Context aufgeloest.
|
||||
# Kein User-Input. FAIL CLOSED wenn State fehlt. Host-SoT ist NICHT Build-Input.
|
||||
REMEDIATION_BUILD_CONTEXT="" # zur Laufzeit aus Gold-Source-Session + Overlay (Staging)
|
||||
REMEDIATION_BUILD_DOCKERFILE="" # zur Laufzeit aus Staging
|
||||
REMEDIATION_STAGING_ROOT="/opt/rq-historical-remediation-staging"
|
||||
REMEDIATION_STAGING_DIR="" # zur Laufzeit = $REMEDIATION_STAGING_ROOT/$SESSION
|
||||
REMEDIATION_BUILD_INPUT_SHA="" # zur Laufzeit deterministisch (Gold-Source + Overlay)
|
||||
# Autorisiertes Runner-Overlay (Punkt 6): EINZIGE Quelle fuer run_backfill_year.py.
|
||||
# Regulaere Datei, kein Symlink, SHA fest gebunden (RUNNER_REMEDIATION_SHA).
|
||||
RUNNER_PATCH_SOURCE="/opt/data/run_backfill_year.py"
|
||||
# Expected Runner-SHAs (Punkt 10/11): ALT = Gold (vor Remediation), NEU = persistent (nach).
|
||||
RUNNER_GOLD_SHA="70d3abb1a92a871e28cc6fb95b9a26944b55dd8df1b72db80642d6881b7b0f20"
|
||||
RUNNER_REMEDIATION_SHA="6cca3c55bb559d75be2f58e59c231003902f124a54ada25582970524599f0752"
|
||||
|
|
@ -413,6 +422,131 @@ load_recovery_source_state() {
|
|||
RECOVERY_BUILD_DOCKERFILE="$GOLD_SOURCE_ROOT/$sess/Dockerfile"
|
||||
}
|
||||
|
||||
# ------------------------------------------------- REMEDIATION BUILD-CONTEXT ROTATION
|
||||
# Phase 13.5 · Remediation Build Context Control-Plane Rotation.
|
||||
# Der Remediation-Build kommt NICHT mehr aus /opt/historical-v2 (Host-SoT), sondern
|
||||
# aus der kontrollierten, immutable Gold-Source-Session + EXAKT EINEM autorisierten
|
||||
# Runner-Overlay (run_backfill_year.py = RUNNER_REMEDIATION_SHA).
|
||||
# Host-SoT bleibt NUR diagnostisch (HOST_SOT_DRIFT=NON-BLOCKING_FOR_REMEDIATION_BUILD).
|
||||
# Kein globaler Pfadtausch: Recovery/No-op/Host-SoT-Verify/Promotion bleiben unveraendert.
|
||||
|
||||
# load_remediation_source_state(): bindet die EINZIGE aktive Gold-Source-Session aus
|
||||
# RECOVERY_SOURCE_STATE (KEIN latest/auto-select/freie Session). Setzt:
|
||||
# REMEDIATION_SESSION_ID, REMEDIATION_STAGING_DIR, REMEDIATION_BUILD_CONTEXT/DOCKERFILE
|
||||
# FAIL CLOSED wenn State fehlt/ungueltig oder Session-Ordner fehlt.
|
||||
load_remediation_source_state() {
|
||||
if [ ! -f "$RECOVERY_SOURCE_STATE" ]; then
|
||||
die "load-remediation-source-state: Kein Recovery-Source-State ($RECOVERY_SOURCE_STATE). NUR nach realem recover (FAIL CLOSED)"
|
||||
fi
|
||||
local sess
|
||||
sess="$(awk -F'=' '$1=="GOLD_SOURCE_SESSION"{print $2}' "$RECOVERY_SOURCE_STATE" 2>/dev/null)"
|
||||
[[ "$sess" =~ ^[0-9]{8}_[0-9]{6}$ ]] || die "load-remediation-source-state: GOLD_SOURCE_SESSION ungueltig/fehlt (FAIL CLOSED)"
|
||||
local sdir="$GOLD_SOURCE_ROOT/$sess"
|
||||
[ -d "$sdir" ] || die "load-remediation-source-state: Gold-Source-Session fehlt $sdir (FAIL CLOSED)"
|
||||
REMEDIATION_SESSION_ID="$sess"
|
||||
REMEDIATION_STAGING_DIR="$REMEDIATION_STAGING_ROOT/$sess"
|
||||
REMEDIATION_BUILD_CONTEXT="$REMEDIATION_STAGING_DIR"
|
||||
REMEDIATION_BUILD_DOCKERFILE="$REMEDIATION_STAGING_DIR/Dockerfile"
|
||||
}
|
||||
|
||||
# remediation_expected_manifest(): erzeugt das ERWARTETE Remediation-Build-Input-Manifest
|
||||
# (zeilenweise '<sha> app/<rel>', konsistent mit GOLD_MANIFEST) aus der Gold-Source-Session
|
||||
# + autorisiertem Runner-Overlay. Basis = Gold-Source-Session (immutable, 63 Gold-Dateien),
|
||||
# dann run_backfill_year.py durch RUNNER_REMEDIATION_SHA ersetzt. KEIN Host-SoT. Read-only.
|
||||
remediation_expected_manifest() {
|
||||
local sdir="$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID"
|
||||
[ -d "$sdir" ] || die "remediation-expected-manifest: Session fehlt $sdir (FAIL CLOSED)"
|
||||
( cd "$sdir" && find app -type f -name '*.py' ! -path '*/__pycache__/*' -printf '%P\n' 2>/dev/null | sort \
|
||||
| while IFS= read -r rel; do
|
||||
[ -f "app/$rel" ] || continue
|
||||
if [ "$rel" = "run_backfill_year.py" ]; then
|
||||
printf '%s app/%s\n' "$RUNNER_REMEDIATION_SHA" "$rel"
|
||||
else
|
||||
printf '%s app/%s\n' "$(sha256sum "app/$rel" | awk '{print $1}')" "$rel"
|
||||
fi
|
||||
done )
|
||||
}
|
||||
|
||||
# remediation_expected_build_input_sha(): deterministischer REMEDIATION_BUILD_INPUT_SHA
|
||||
# ueber den finalen tatsaechlich gebauten Input: 62 unveraenderte Gold-Dateien + Runner
|
||||
# 6cca3c55... + Dockerfile + requirements.txt + docker-compose.yml + .dockerignore.
|
||||
# Basis = $1 (default: Gold-Source-Session, immutable), Runner-Overlay ersetzt. KEIN Host-SoT.
|
||||
remediation_expected_build_input_sha() {
|
||||
local sdir="${1:-$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID}"
|
||||
[ -d "$sdir" ] || die "remediation-build-input-sha: Basis fehlt $sdir (FAIL CLOSED)"
|
||||
( cd "$sdir" && find . -type f ! -name 'session.meta' ! -name 'application.manifest' ! -name 'build-support.manifest' ! -name 'final-build-input.manifest' -printf '%P\n' 2>/dev/null | sort \
|
||||
| while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
if [ "$f" = "app/run_backfill_year.py" ]; then
|
||||
printf '%s %s\n' "$RUNNER_REMEDIATION_SHA" "$f"
|
||||
else
|
||||
printf '%s %s\n' "$(sha256sum "$f" | awk '{print $1}')" "$f"
|
||||
fi
|
||||
done ) | sha256sum | awk '{print $1}'
|
||||
}
|
||||
|
||||
# remediation_expected_manifest_ok(): klassifiziert das ERWARTETE Remediation-Build-Input
|
||||
# (Gold-Source-Session + autorisierter Runner-Overlay, via remediation_expected_manifest)
|
||||
# gegen GOLD_MANIFEST. Erwartung: 62 Gold-MATCH + 1 AUTHORIZED_DIVERGED (Runner 6cca3c55)
|
||||
# + 0 UNAUTH + 0 MISSING + 0 EXTRA. Setzt globale Zaehler RM_MATCH/RM_AUTH/RM_UNAUTH/
|
||||
# RM_MISSING/RM_EXTRA. Read-only. FAIL CLOSED bei jeder Abweichung.
|
||||
remediation_expected_manifest_ok() {
|
||||
local sdir="$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID"
|
||||
[ -d "$sdir" ] || die "remediation-manifest-ok: Session fehlt $sdir (FAIL CLOSED)"
|
||||
local -A gsha
|
||||
while read -r sha p; do gsha["$p"]="$sha"; done < "$GOLD_MANIFEST"
|
||||
RM_MATCH=0; RM_AUTH=0; RM_UNAUTH=0; RM_MISSING=0; RM_EXTRA=0
|
||||
local path g s
|
||||
# Gold-Dateien: 62 MATCH + 1 AUTHORIZED (Runner 6cca3c55)
|
||||
for path in "${!gsha[@]}"; do
|
||||
g="${gsha[$path]}"
|
||||
if [ "$path" = "app/run_backfill_year.py" ]; then
|
||||
# autorisierte Divergenz: Gold 70d3abb1... -> Remediation 6cca3c55...
|
||||
s="$(remediation_expected_manifest | awk -v r="$path" '$2==r{print $1}')"
|
||||
if [ "$s" = "$RUNNER_REMEDIATION_SHA" ]; then RM_AUTH=$((RM_AUTH+1))
|
||||
elif [ "$s" = "$g" ]; then RM_MATCH=$((RM_MATCH+1))
|
||||
else RM_UNAUTH=$((RM_UNAUTH+1)); fi
|
||||
else
|
||||
s="$(remediation_expected_manifest | awk -v r="$path" '$2==r{print $1}')"
|
||||
if [ -z "$s" ]; then RM_MISSING=$((RM_MISSING+1))
|
||||
elif [ "$s" = "$g" ]; then RM_MATCH=$((RM_MATCH+1))
|
||||
else RM_UNAUTH=$((RM_UNAUTH+1)); fi
|
||||
fi
|
||||
done
|
||||
# Extra-Dateien (forbidden extras + unerlaubte) im ERWARTETEN Build-Input
|
||||
local rel
|
||||
while IFS= read -r rel; do
|
||||
[ -n "$rel" ] || continue
|
||||
case "$rel" in
|
||||
app/app/backfill.py|app/app/test_idempotency.py|app/eligibility.py) RM_EXTRA=$((RM_EXTRA+1)) ;;
|
||||
app/*) [ -n "${gsha[$rel]:-}" ] || RM_EXTRA=$((RM_EXTRA+1)) ;;
|
||||
esac
|
||||
done < <( remediation_expected_manifest | awk '{print $2}' | sort )
|
||||
[ "$RM_AUTH" = "1" ] && [ "$RM_UNAUTH" = "0" ] && [ "$RM_MISSING" = "0" ] && [ "$RM_EXTRA" = "0" ]
|
||||
}
|
||||
|
||||
# remediation_staging_matches_expected(): manifestiert das TATSÄCHLICHE Staging-Dir
|
||||
# (nach Overlay) und vergleicht es exakt mit dem erwarteten Build-Input
|
||||
# (remediation_expected_manifest). Zusaetzlich: forbidden extras im Staging ABSENT.
|
||||
# Read-only. FAIL CLOSED bei jeder Abweichung.
|
||||
remediation_staging_matches_expected() {
|
||||
local sdir="$REMEDIATION_STAGING_DIR"
|
||||
[ -d "$sdir" ] || die "remediation-staging-check: Staging fehlt $sdir (FAIL CLOSED)"
|
||||
local exp act
|
||||
exp="$(remediation_expected_manifest | sort)"
|
||||
act="$( cd "$sdir" && find app -type f -name '*.py' ! -path '*/__pycache__/*' -printf '%P\n' 2>/dev/null | sort \
|
||||
| while IFS= read -r rel; do
|
||||
[ -f "app/$rel" ] || continue
|
||||
printf '%s app/%s\n' "$(sha256sum "app/$rel" | awk '{print $1}')" "$rel"
|
||||
done )"
|
||||
[ "$act" = "$exp" ] || die "remediation-staging-check: Staging != erwartetes Build-Input (FAIL CLOSED)"
|
||||
# forbidden extras im Staging zwingend ABSENT
|
||||
for fe in app/app/backfill.py app/app/test_idempotency.py app/eligibility.py; do
|
||||
[ -e "$sdir/$fe" ] && die "remediation-staging-check: forbidden extra $fe im Staging (FAIL CLOSED)"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# write_recovery_image_state(): atomarer Write des Recovery-IMAGE-State NACH erfolgreichem Build.
|
||||
# $1 = FULL_IMAGE_ID (sha256:...). Verlangt: State ABSENT (kein Overwrite, FAIL CLOSED).
|
||||
# Schreibt via Temp-Datei im selben Filesystem + fsync + atomares mv. Kein partial state sichtbar.
|
||||
|
|
@ -1331,22 +1465,23 @@ remediation_full_image_id() { # FULL_IMAGE_ID aus Session (echo) — nur wenn r
|
|||
}
|
||||
# Pre-Build-Guards (Punkt 12: dry-run-Gates A–O). Read-only; alle FAIL CLOSED.
|
||||
remediation_guards() {
|
||||
load_remediation_source_state
|
||||
load_recovery_state
|
||||
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 " [guard A] Gold-Source-Session (immutable) gebunden:"
|
||||
[ -d "$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID" ] || die "Guard A FAIL: Gold-Source-Session fehlt (FAIL CLOSED)"
|
||||
echo " OK (Session $REMEDIATION_SESSION_ID)"
|
||||
echo " [guard B] Runner-Overlay-SHA == erwartet (6cca3c55...):"
|
||||
local hs; hs="$(sha256sum "$RUNNER_PATCH_SOURCE" | awk '{print $1}')"
|
||||
[ "$hs" = "$RUNNER_REMEDIATION_SHA" ] || die "Guard B FAIL: Runner-Overlay-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 " [guard C] Build-Context-Basis (Gold-Source-Session) + Dockerfile/.dockerignore:"
|
||||
[ -d "$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID" ] || die "Guard C FAIL: Gold-Source-Session fehlt (FAIL CLOSED)"
|
||||
[ -f "$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID/Dockerfile" ] || die "Guard C FAIL: Dockerfile fehlt (FAIL CLOSED)"
|
||||
[ -f "$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID/.dockerignore" ] || die "Guard C FAIL: .dockerignore fehlt (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 D] erwartetes Build-Input (62 Gold + 1 autorisierter Runner-Overlay):"
|
||||
remediation_expected_manifest_ok || die "Guard D FAIL: erwartetes Remediation-Build-Input nicht exakt (FAIL CLOSED)"
|
||||
echo " OK (MATCH=$RM_MATCH AUTHORIZED_DIVERGED=$RM_AUTH UNAUTHORIZED_DIVERGED=$RM_UNAUTH MISSING=$RM_MISSING EXTRA=$RM_EXTRA)"
|
||||
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))"
|
||||
|
|
@ -1360,10 +1495,19 @@ remediation_guards() {
|
|||
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:"
|
||||
echo " [guard I] No-op-Candidate (DIAGNOSTIC, NON-BLOCKING fuer Remediation-Build):"
|
||||
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)"
|
||||
if [ -n "$ni" ] && [ "$ni" = "$CANDIDATE_IMAGE_ID" ] && [ "$CANDIDATE_IMAGE_ID" = "$REM_NOOP_IMAGE_ID" ]; then
|
||||
echo " NOOP_CANDIDATE_TAG=PRESENT"
|
||||
echo " STATUS=OK"
|
||||
else
|
||||
echo " NOOP_CANDIDATE_TAG=ABSENT"
|
||||
echo " CAUSE=PREEXISTING_COOLIFY_CLEANUP"
|
||||
echo " IMPACT_ON_REMEDIATION_BUILD=NONE"
|
||||
echo " STATUS=NON_BLOCKING_DIAGNOSTIC"
|
||||
echo " (No-op-Candidate ist weder Build Source, Build Input, Overlay Source,"
|
||||
echo " Recovery Anchor noch Remediation Target Identity -> kein fail-closed Build-Gate)"
|
||||
fi
|
||||
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)"
|
||||
|
|
@ -1376,10 +1520,19 @@ remediation_guards() {
|
|||
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 N] Dockerfile/Context exakt erwartet (aus Gold-Source-Session):"
|
||||
local gdf="$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID/Dockerfile"
|
||||
[ -f "$gdf" ] || die "Guard N FAIL: Gold-Source-Dockerfile fehlt (FAIL CLOSED)"
|
||||
grep -qE '^FROM python:3\.11-slim' "$gdf" || die "Guard N FAIL: Dockerfile Base-Image python:3.11-slim fehlt (FAIL CLOSED)"
|
||||
grep -qE '^COPY app/ \./app/' "$gdf" || die "Guard N FAIL: Dockerfile COPY app/ ./app/ fehlt (FAIL CLOSED)"
|
||||
grep -qE '^EXPOSE 8000' "$gdf" || die "Guard N FAIL: Dockerfile EXPOSE 8000 fehlt (FAIL CLOSED)"
|
||||
echo " OK"
|
||||
echo " [guard O] kein freier Parameter/User-Input:"
|
||||
echo " OK (keine Argumente akzeptiert)"
|
||||
echo " [diagnostic] Host-SoT (NICHT Build-Input):"
|
||||
local hrepo; hrepo="$(sha256sum /opt/historical-v2/app/persistence/repository.py 2>/dev/null | awk '{print $1}')"
|
||||
echo " HOST_SOT_DRIFT=DETECTED (repository.py $hrepo vs Gold 8fff892f...)"
|
||||
echo " HOST_SOT_DRIFT=NON-BLOCKING_FOR_REMEDIATION_BUILD"
|
||||
}
|
||||
# ---- GEMEINSAME AUTHORIZED-GOLD-DELTA-POLICY (Phase 13.5) ----
|
||||
# Einzige autorisierte Divergenz gegen GOLD_MANIFEST:
|
||||
|
|
@ -1423,31 +1576,70 @@ authorized_gold_delta() {
|
|||
# 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() {
|
||||
load_remediation_source_state
|
||||
echo " BUILD_SOURCE=VERIFIED_GOLD_SOURCE"
|
||||
echo " SOURCE_SESSION=$REMEDIATION_SESSION_ID"
|
||||
echo " AUTHORIZED_OVERLAY=run_backfill_year.py"
|
||||
echo " AUTHORIZED_RUNNER_SHA=$RUNNER_REMEDIATION_SHA"
|
||||
local hm
|
||||
hm="$(mktemp)"; gen_host_manifest > "$hm"
|
||||
hm="$(mktemp)"; remediation_expected_manifest > "$hm"
|
||||
authorized_gold_delta "$hm"
|
||||
rm -f "$hm"
|
||||
echo " Gold $((AD_MATCH+AD_AUTH+AD_UNAUTH+AD_MISSING)): MATCH=$AD_MATCH AUTHORIZED_DIVERGED=$AD_AUTH UNAUTHORIZED_DIVERGED=$AD_UNAUTH MISSING=$AD_MISSING ; unerlaubte EXTRA=$AD_EXTRA"
|
||||
[ "$AD_AUTH" = "1" ] || die "BUILD-INPUT FAIL: autorisierte Runner-Divergenz fehlt (FAIL CLOSED)"
|
||||
[ "$AD_RET" = "ok" ] || die "BUILD-INPUT FAIL: unerwartete Divergenz (FAIL CLOSED)"
|
||||
echo " [diagnostic] Host-SoT (NICHT Build-Input):"
|
||||
local hrepo; hrepo="$(sha256sum /opt/historical-v2/app/persistence/repository.py 2>/dev/null | awk '{print $1}')"
|
||||
echo " HOST_SOT_DRIFT=DETECTED (repository.py $hrepo vs Gold 8fff892f...)"
|
||||
echo " HOST_SOT_DRIFT=NON-BLOCKING_FOR_REMEDIATION_BUILD"
|
||||
return 0
|
||||
}
|
||||
# ---- build-runner-remediation (DRY_RUN = kein Build; KEIN Overwrite) ----
|
||||
# ---- build-runner-remediation (DRY_RUN = kein Build, kein Staging; KEIN Overwrite) ----
|
||||
# Build-Context = kontrolliertes Staging aus der immutable Gold-Source-Session
|
||||
# + EXAKT EIN autorisierter Runner-Overlay (RUNNER_PATCH_SOURCE). KEIN /opt/historical-v2.
|
||||
# DRY_RUN: Guards + erwartetes Manifest + deterministischer REMEDIATION_BUILD_INPUT_SHA,
|
||||
# ZERO MUTATION (kein Staging-Dir erzeugt). Real: Staging erzeugen, Guards, Build.
|
||||
build_runner_remediation() {
|
||||
expect_zero "$@"
|
||||
echo "=== BUILD-RUNNER-REMEDIATION ($RUN_MODE) ==="
|
||||
echo " Build Context : $REMEDIATION_BUILD_CONTEXT (fix)"
|
||||
echo " Dockerfile : $REMEDIATION_BUILD_DOCKERFILE (fix)"
|
||||
load_remediation_source_state
|
||||
echo " BUILD_SOURCE=VERIFIED_GOLD_SOURCE"
|
||||
echo " SOURCE_SESSION=$REMEDIATION_SESSION_ID"
|
||||
echo " AUTHORIZED_OVERLAY=run_backfill_year.py"
|
||||
echo " AUTHORIZED_RUNNER_SHA=$RUNNER_REMEDIATION_SHA"
|
||||
echo " Build Context : $REMEDIATION_BUILD_CONTEXT (Staging aus Gold-Source-Session)"
|
||||
echo " Dockerfile : $REMEDIATION_BUILD_DOCKERFILE (aus Gold-Source-Session)"
|
||||
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
|
||||
echo " [expected-manifest] REMEDIATION_BUILD_INPUT (Gold-Source + Overlay):"
|
||||
remediation_expected_manifest | while IFS= read -r line; do echo " $line"; done
|
||||
echo " [expected] MATCH=$RM_MATCH AUTHORIZED_DIVERGED=$RM_AUTH UNAUTHORIZED_DIVERGED=$RM_UNAUTH MISSING=$RM_MISSING EXTRA=$RM_EXTRA"
|
||||
local ebi; ebi="$(remediation_expected_build_input_sha)"
|
||||
echo " REMEDIATION_BUILD_INPUT_SHA=$ebi (deterministisch, Gold-Source + Overlay)"
|
||||
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)."
|
||||
echo "DRY_RUN_PASS: build-runner-remediation wuerde Staging aus Gold-Source-Session erzeugen, run_backfill_year.py durch $RUNNER_REMEDIATION_SHA ersetzen und docker build -f $REMEDIATION_BUILD_DOCKERFILE -t $REMEDIATION_TAG $REMEDIATION_BUILD_CONTEXT ausfuehren (keine Aktion)."
|
||||
exit 0
|
||||
fi
|
||||
mode_or_die
|
||||
# --- Staging erzeugen (Punkt 7/8): Gold-Source-Session -> kontrollierte Kopie -> Overlay ---
|
||||
if [ -e "$REMEDIATION_STAGING_DIR" ]; then
|
||||
die "build: Staging-Ziel existiert bereits unerwartet $REMEDIATION_STAGING_DIR (FAIL CLOSED)"
|
||||
fi
|
||||
mkdir -p "$REMEDIATION_STAGING_ROOT" || die "build: Staging-Root nicht anlegbar (FAIL CLOSED)"
|
||||
cp -a "$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID" "$REMEDIATION_STAGING_DIR" || die "build: Gold-Source-Kopie fehlgeschlagen (FAIL CLOSED)"
|
||||
# Overlay: run_backfill_year.py durch autorisierten Patch ersetzen (regulaere Datei, kein Symlink)
|
||||
[ -L "$RUNNER_PATCH_SOURCE" ] && die "build: Runner-Patch ist Symlink (FAIL CLOSED)"
|
||||
[ -f "$RUNNER_PATCH_SOURCE" ] || die "build: Runner-Patch fehlt (FAIL CLOSED)"
|
||||
cp -f "$RUNNER_PATCH_SOURCE" "$REMEDIATION_STAGING_DIR/app/run_backfill_year.py" || die "build: Overlay-Kopie fehlgeschlagen (FAIL CLOSED)"
|
||||
# Staging verifizieren: erwartetes Manifest + Build-Input-SHA
|
||||
REMEDIATION_SESSION_ID="$REMEDIATION_SESSION_ID"
|
||||
REMEDIATION_BUILD_CONTEXT="$REMEDIATION_STAGING_DIR"
|
||||
REMEDIATION_BUILD_DOCKERFILE="$REMEDIATION_STAGING_DIR/Dockerfile"
|
||||
remediation_staging_matches_expected || die "build: Staging != erwartetes Build-Input (FAIL CLOSED)"
|
||||
local sbi; sbi="$(remediation_expected_build_input_sha)"
|
||||
[ "$sbi" = "$ebi" ] || die "build: Staging-Build-Input-SHA $sbi != erwartet $ebi (FAIL CLOSED)"
|
||||
echo " [staging] Staging verifiziert: MATCH=$RM_MATCH AUTHORIZED_DIVERGED=$RM_AUTH UNAUTHORIZED_DIVERGED=$RM_UNAUTH MISSING=$RM_MISSING EXTRA=$RM_EXTRA"
|
||||
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
|
||||
|
|
@ -1460,9 +1652,10 @@ build_runner_remediation() {
|
|||
cat > "$REMEDIATION_SESSION_META" <<EOF
|
||||
TAG=$REMEDIATION_TAG
|
||||
FULL_IMAGE_ID=$newid
|
||||
BUILD_INPUT_SHA=$(build_context_manifest | sha256sum | awk '{print $1}')
|
||||
REMEDIATION_BUILD_INPUT_SHA=$sbi
|
||||
SOURCE_SESSION=$REMEDIATION_SESSION_ID
|
||||
AUTHORIZED_RUNNER_SHA=$RUNNER_REMEDIATION_SHA
|
||||
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"
|
||||
|
|
@ -1481,7 +1674,7 @@ verify_runner_remediation_image() {
|
|||
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):"
|
||||
echo " [Gold-Delta] Image vs GOLD_MANIFEST (62 MATCH + 1 autorisierter Runner-Overlay):"
|
||||
local tmpc tmpdir tmpf
|
||||
tmpc="rq-rem-verify-$(date +%s)"; tmpdir="/tmp/rq-rem-copy-$$"
|
||||
rm -rf "$tmpdir"; mkdir -p "$tmpdir"
|
||||
|
|
@ -1496,33 +1689,23 @@ verify_runner_remediation_image() {
|
|||
echo " Image vs GOLD: MATCH=$AD_MATCH AUTHORIZED_DIVERGED=$AD_AUTH UNAUTHORIZED_DIVERGED=$AD_UNAUTH MISSING=$AD_MISSING unerlaubte EXTRA=$AD_EXTRA"
|
||||
[ "$AD_AUTH" = "1" ] || { rm -rf "$tmpdir"; die "verify: Gold-Delta != exakt 1 autorisierte Runner-Divergenz (FAIL CLOSED)"; }
|
||||
[ "$AD_RET" = "ok" ] || { rm -rf "$tmpdir"; die "verify: Image != Gold-Manifest (unzulaessige Divergenz) (FAIL CLOSED)"; }
|
||||
# [Candidate vs Host-SoT] Image-App-Manifest == Host-SoT-App-Manifest (direkter
|
||||
# Manifest-zu-Manifest-Vergleich, OHNE Gold-Bezug; vollstaendig MATCH).
|
||||
# Host-only-Legacy-Dateien (im Image via .dockerignore ausgeschlossen) sind erlaubt.
|
||||
local hmf cmiss cextra
|
||||
hmf="$(mktemp)"; gen_host_manifest > "$hmf"
|
||||
cmiss=0; cextra=0
|
||||
while read -r hsha hrel; do
|
||||
hrel="${hrel#./}"; [ -n "$hrel" ] || continue
|
||||
case "$hrel" in
|
||||
app/app/backfill.py|app/app/test_idempotency.py|app/eligibility.py) continue ;;
|
||||
esac
|
||||
isha="$(awk -v r="$hrel" '$2==r{print $1}' "$tmpf")"
|
||||
if [ -z "$isha" ]; then cmiss=$((cmiss+1))
|
||||
elif [ "$isha" != "$hsha" ]; then cextra=$((cextra+1)); fi
|
||||
done < "$hmf"
|
||||
rm -f "$hmf"
|
||||
echo " Candidate vs Host-SoT: MATCH (direkt) MISSING=$cmiss DIVERGED=$cextra"
|
||||
[ "$cmiss" = "0" ] && [ "$cextra" = "0" ] || { rm -rf "$tmpdir"; die "verify: Candidate != Host-SoT (FAIL CLOSED)"; }
|
||||
# [repository hard gate] app/persistence/repository.py im Image MUSS Gold sein (8fff892f...)
|
||||
local irepo; irepo="$(awk '$2=="app/persistence/repository.py"{print $1}' "$tmpf")"
|
||||
[ "$irepo" = "8fff892f52a612f464b5be21c66ef7787a47f2d7d049f815ef87837553c2a228" ] \
|
||||
|| { rm -rf "$tmpdir"; die "verify: repository.py im Image != Gold (8fff892f...) (FAIL CLOSED)"; }
|
||||
echo " repository.py = Gold (8fff892f...) OK"
|
||||
# [diagnostic] Host-SoT (NICHT Build-Input): repository.py 3fc0f962... vs Gold 8fff892f...
|
||||
local hrepo; hrepo="$(sha256sum /opt/historical-v2/app/persistence/repository.py 2>/dev/null | awk '{print $1}')"
|
||||
echo " HOST_SOT_DRIFT=DETECTED (repository.py $hrepo vs Gold 8fff892f...)"
|
||||
echo " HOST_SOT_DRIFT=NON-BLOCKING_FOR_REMEDIATION_BUILD"
|
||||
rm -f "$tmpf"
|
||||
echo " [Critical-Files] $REMEDIATION_TAG vs gold:"
|
||||
verify_critical_files "$tmpdir/appapp" runner
|
||||
rm -rf "$tmpdir"
|
||||
echo " [Runner] Image vs Host-SoT persistent:"
|
||||
echo " [Runner] Image == autorisierter Overlay (6cca3c55...):"
|
||||
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)"
|
||||
[ "$ir" = "$RUNNER_REMEDIATION_SHA" ] || die "verify: Runner-Image-SHA ($ir) != $RUNNER_REMEDIATION_SHA (FAIL CLOSED)"
|
||||
echo " Image=$ir (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:"
|
||||
|
|
@ -1534,11 +1717,16 @@ verify_runner_remediation_image() {
|
|||
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 (Candidate==Host-SoT; Gold-Delta exakt 1 autorisierte Runner-Divergenz; Critical-SHAs + Config PASS)"
|
||||
echo " [Build-Input] Session-Binding (Gold-Source + Overlay):"
|
||||
local ssrc srunner sbi
|
||||
ssrc="$(grep '^SOURCE_SESSION=' "$REMEDIATION_SESSION_META" | cut -d= -f2)"
|
||||
srunner="$(grep '^AUTHORIZED_RUNNER_SHA=' "$REMEDIATION_SESSION_META" | cut -d= -f2)"
|
||||
sbi="$(grep '^REMEDIATION_BUILD_INPUT_SHA=' "$REMEDIATION_SESSION_META" | cut -d= -f2)"
|
||||
[ "$ssrc" = "$REMEDIATION_SESSION_ID" ] || die "verify: Session-SOURCE_SESSION != erwartet (FAIL CLOSED)"
|
||||
[ "$srunner" = "$RUNNER_REMEDIATION_SHA" ] || die "verify: Session-AUTHORIZED_RUNNER_SHA != erwartet (FAIL CLOSED)"
|
||||
[[ "$sbi" =~ ^[a-f0-9]{64}$ ]] || die "verify: Session-REMEDIATION_BUILD_INPUT_SHA ungueltig (FAIL CLOSED)"
|
||||
echo " SOURCE_SESSION=$ssrc AUTHORIZED_RUNNER_SHA=$srunner REMEDIATION_BUILD_INPUT_SHA=$sbi"
|
||||
echo "VERDICT: REMEDIATION IMAGE OK (62 Gold-MATCH + 1 autorisierter Runner-Overlay; repository.py Gold; Critical-SHAs + Config PASS)"
|
||||
exit 0
|
||||
}
|
||||
# ---- recreate-runner-remediation-safe (NUR historical-service; nur Red Queen) ----
|
||||
|
|
@ -1726,7 +1914,13 @@ verify_archive() { # $1 = role (read-only)
|
|||
adir="$(role_arch_dir "$role")"
|
||||
case "$role" in
|
||||
recovery) sid="$RECOVERY_ARCHIVE_ID" ;;
|
||||
remediation) sid="$REMEDIATION_ARCHIVE_ID" ;;
|
||||
remediation)
|
||||
# Aktive Remediation-Identitaet aus der Build-Session (Punkt 21). Keine
|
||||
# historische 41e357a1... als aktive Identity. FAIL CLOSED ohne Build-Session.
|
||||
remediation_session || die "verify-archive(remediation): keine gueltige Build-Session (FAIL CLOSED)"
|
||||
local rid; rid="$(remediation_full_image_id)"
|
||||
sid="${rid#sha256:}"; sid="${sid:0:12}"
|
||||
;;
|
||||
esac
|
||||
# Archive existiert
|
||||
[ -d "$adir" ] || die "verify-archive($role): kein Archiv-Root (FAIL CLOSED)"
|
||||
|
|
|
|||
Loading…
Reference in a new issue