Phase 13.5 Recovery Rebuild Guard Source Rotation: rebuild_guards Guard A/B/C auf Gold-Source-Session (verify-recovered-gold-build-source + verify_recovery_build_input), Host-SoT-Drift non-blocking Diagnostic, kein /opt/historical-v2 als Recovery-Build-Blocker
This commit is contained in:
parent
d7db920116
commit
a9d7da14f5
1 changed files with 156 additions and 12 deletions
|
|
@ -208,6 +208,12 @@ RECOVERY_IMAGE_STATE="$CONTROL_PLANE_STATE_DIR/recovery-image.state"
|
|||
GOLD_DOCKERIGNORE_SHA="70bac51783f5ee956796cf2216ab17a550fcb646092edad3504763dbfabb881f"
|
||||
# Historische Recovery-Image-ID (VERLOREN) — NUR Evidence, KEINE zwingende Output-ID.
|
||||
HISTORICAL_RECOVERY_IMAGE_ID="sha256:d2db926dc6f5f7cf2f8e85d271a9b16fcc3cfb54b6557604d3d784f7961a4bc2"
|
||||
# Recovery-Source-State-Felder (zur Laufzeit via load_recovery_source_state gesetzt).
|
||||
# NUR der SOURCE-State (existiert VOR Build); der IMAGE-State existiert erst NACH Build.
|
||||
RECOVERY_GOLD_SOURCE_SESSION="" # GOLD_SOURCE_SESSION aus RECOVERY_SOURCE_STATE
|
||||
RECOVERY_GOLD_BUILD_INPUT_SHA="" # FINAL_BUILD_INPUT_SHA aus RECOVERY_SOURCE_STATE
|
||||
RECOVERY_GOLD_SOURCE_CONTAINER_ID="" # SOURCE_CONTAINER_ID aus RECOVERY_SOURCE_STATE
|
||||
RECOVERY_GOLD_SOURCE_IMAGE_ID="" # SOURCE_IMAGE_ID aus RECOVERY_SOURCE_STATE
|
||||
|
||||
|
||||
# ------------------------------------------------- PERSISTENT-HOST-SOT-PROMOTION
|
||||
|
|
@ -382,6 +388,59 @@ load_recovery_state() {
|
|||
RECOVERY_BUILD_DOCKERFILE="$GOLD_SOURCE_ROOT/$sess/Dockerfile"
|
||||
}
|
||||
|
||||
# load_recovery_source_state(): laedt NUR den Recovery-SOURCE-State (existiert VOR Build).
|
||||
# Setzt RECOVERY_BUILD_CONTEXT/DOCKERFILE + die Source-Identitaets-Felder.
|
||||
# Verlangt NICHT den Recovery-IMAGE-State (der existiert erst NACH Build).
|
||||
# FAIL CLOSED wenn SOURCE-State fehlt/ungueltig. Kein freier Input, kein statischer Fallback.
|
||||
load_recovery_source_state() {
|
||||
if [ ! -f "$RECOVERY_SOURCE_STATE" ]; then
|
||||
die "load-recovery-source-state: Kein Recovery-Source-State ($RECOVERY_SOURCE_STATE). NUR nach realem recover (FAIL CLOSED)"
|
||||
fi
|
||||
local sess sha cid iid
|
||||
sess="$(awk -F'=' '$1=="GOLD_SOURCE_SESSION"{print $2}' "$RECOVERY_SOURCE_STATE" 2>/dev/null)"
|
||||
[[ "$sess" =~ ^[0-9]{8}_[0-9]{6}$ ]] || die "load-recovery-source-state: GOLD_SOURCE_SESSION ungueltig/fehlt (FAIL CLOSED)"
|
||||
sha="$(awk -F'=' '$1=="FINAL_BUILD_INPUT_SHA"{print $2}' "$RECOVERY_SOURCE_STATE" 2>/dev/null)"
|
||||
[[ "$sha" =~ ^[a-f0-9]{64}$ ]] || die "load-recovery-source-state: FINAL_BUILD_INPUT_SHA ungueltig/fehlt (FAIL CLOSED)"
|
||||
cid="$(awk -F'=' '$1=="SOURCE_CONTAINER_ID"{print $2}' "$RECOVERY_SOURCE_STATE" 2>/dev/null)"
|
||||
[[ "$cid" =~ ^[a-f0-9]{64}$ ]] || die "load-recovery-source-state: SOURCE_CONTAINER_ID ungueltig/fehlt (FAIL CLOSED)"
|
||||
iid="$(awk -F'=' '$1=="SOURCE_IMAGE_ID"{print $2}' "$RECOVERY_SOURCE_STATE" 2>/dev/null)"
|
||||
[[ "$iid" =~ ^sha256:[a-f0-9]{64}$ ]] || die "load-recovery-source-state: SOURCE_IMAGE_ID ungueltig/fehlt (FAIL CLOSED)"
|
||||
RECOVERY_GOLD_SOURCE_SESSION="$sess"
|
||||
RECOVERY_GOLD_BUILD_INPUT_SHA="$sha"
|
||||
RECOVERY_GOLD_SOURCE_CONTAINER_ID="$cid"
|
||||
RECOVERY_GOLD_SOURCE_IMAGE_ID="$iid"
|
||||
RECOVERY_BUILD_CONTEXT="$GOLD_SOURCE_ROOT/$sess"
|
||||
RECOVERY_BUILD_DOCKERFILE="$GOLD_SOURCE_ROOT/$sess/Dockerfile"
|
||||
}
|
||||
|
||||
# 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.
|
||||
write_recovery_image_state() {
|
||||
local fullid="$1" tmp
|
||||
[[ "$fullid" =~ ^sha256:[a-f0-9]{64}$ ]] || die "write-recovery-image-state: FULL_IMAGE_ID ungueltig (FAIL CLOSED)"
|
||||
mkdir -p "$CONTROL_PLANE_STATE_DIR" || die "write-recovery-image-state: State-Dir nicht anlegbar (FAIL CLOSED)"
|
||||
if [ -e "$RECOVERY_IMAGE_STATE" ]; then
|
||||
die "write-recovery-image-state: Recovery-Image-State existiert bereits $RECOVERY_IMAGE_STATE (KEIN Overwrite, FAIL CLOSED)"
|
||||
fi
|
||||
tmp="$CONTROL_PLANE_STATE_DIR/.recovery-image.state.tmp.$$"
|
||||
{
|
||||
echo "NEW_RECOVERY_FULL_IMAGE_ID=$fullid"
|
||||
echo "RECOVERY_TAG=$RECOVERY_REBUILD_TAG"
|
||||
echo "BUILD_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "BUILD_INPUT_SHA=$RECOVERY_GOLD_BUILD_INPUT_SHA"
|
||||
echo "SOURCE_SESSION=$RECOVERY_GOLD_SOURCE_SESSION"
|
||||
echo "SOURCE_CONTAINER_ID=$RECOVERY_GOLD_SOURCE_CONTAINER_ID"
|
||||
echo "SOURCE_IMAGE_ID=$RECOVERY_GOLD_SOURCE_IMAGE_ID"
|
||||
echo "WRAPPER_SHA=$(sha256sum /usr/local/bin/rq-historical | awk '{print $1}')"
|
||||
} > "$tmp" || { rm -f "$tmp"; die "write-recovery-image-state: Temp-Write fehlgeschlagen (FAIL CLOSED)"; }
|
||||
chown root:root "$tmp" 2>/dev/null || true
|
||||
chmod 600 "$tmp" || { rm -f "$tmp"; die "write-recovery-image-state: chmod fehlgeschlagen (FAIL CLOSED)"; }
|
||||
sync -f "$tmp" 2>/dev/null || true
|
||||
mv -f "$tmp" "$RECOVERY_IMAGE_STATE" || { rm -f "$tmp"; die "write-recovery-image-state: atomares mv fehlgeschlagen (FAIL CLOSED)"; }
|
||||
sync -f "$RECOVERY_IMAGE_STATE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# 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
|
||||
|
|
@ -605,14 +664,76 @@ verify_host_gold_context() {
|
|||
[ $d -eq 0 ] && [ $m -eq 0 ] && [ $x -eq 0 ]
|
||||
}
|
||||
|
||||
# verify_recovery_build_input(): beweist den tatsaechlichen finalen Build Input der
|
||||
# Gold-Source-Session (NICHT Host-SoT). Prueft exakt:
|
||||
# - Build-Support SHAs (Dockerfile, requirements.txt, .dockerignore) gegen GOLD_BUILD_SUPPORT
|
||||
# - .dockerignore exakt (GOLD_DOCKERIGNORE_SHA)
|
||||
# - Dockerfile exakt erwartet
|
||||
# - keine forbidden extras in der Session
|
||||
# - FINAL_BUILD_INPUT_SHA exakt (aus Control-Plane-SOURCE-State)
|
||||
# FAIL CLOSED bei jeder Abweichung. Kein freier Input, kein /opt/historical-v2.
|
||||
verify_recovery_build_input() {
|
||||
local ctx="$RECOVERY_BUILD_CONTEXT"
|
||||
[ -d "$ctx" ] || die "verify-recovery-build-input: Build-Context fehlt $ctx (FAIL CLOSED)"
|
||||
# Build-Support SHAs exakt (gegen die Session-Dateien, nicht Host)
|
||||
local key spec src exp f s
|
||||
for key in "${!GOLD_BUILD_SUPPORT[@]}"; do
|
||||
spec="${GOLD_BUILD_SUPPORT[$key]}"; src="${spec%%|*}"; exp="${spec##*|}"
|
||||
f="$ctx/$(basename "$src")"
|
||||
[ -f "$f" ] || die "verify-recovery-build-input: fehlt $f (FAIL CLOSED)"
|
||||
s="$(sha256sum "$f" | awk '{print $1}')"
|
||||
[ "$s" = "$exp" ] || die "verify-recovery-build-input: Build-Support SHA mismatch $key (FAIL CLOSED)"
|
||||
done
|
||||
# .dockerignore exakt
|
||||
local dis
|
||||
dis="$(sha256sum "$ctx/.dockerignore" 2>/dev/null | awk '{print $1}')"
|
||||
[ "$dis" = "$GOLD_DOCKERIGNORE_SHA" ] || die "verify-recovery-build-input: .dockerignore SHA mismatch (FAIL CLOSED)"
|
||||
# Dockerfile exakt erwartet
|
||||
dockerfile_expected || die "verify-recovery-build-input: Dockerfile nicht exakt erwartet (FAIL CLOSED)"
|
||||
# keine forbidden extras in der Session
|
||||
local rel
|
||||
while IFS= read -r rel; do
|
||||
[ -n "$rel" ] || continue
|
||||
if [ -n "${GOLD_FORBIDDEN_EXTRA[$rel]:-}" ]; then
|
||||
die "verify-recovery-build-input: forbidden extra $rel (FAIL CLOSED)"
|
||||
fi
|
||||
done < <( cd "$ctx" && find . -type f -printf '%P\n' 2>/dev/null | sort )
|
||||
# FINAL_BUILD_INPUT_SHA exakt (aus SOURCE-State)
|
||||
local got
|
||||
got="$( ( cd "$ctx" && 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" ] && sha256sum "$f"; done ) | sha256sum | awk '{print $1}' )"
|
||||
[ "$got" = "$RECOVERY_GOLD_BUILD_INPUT_SHA" ] || die "verify-recovery-build-input: FINAL_BUILD_INPUT_SHA mismatch (Source veraendert) (FAIL CLOSED)"
|
||||
return 0
|
||||
}
|
||||
|
||||
rebuild_guards() {
|
||||
echo " [guard A] verify-sot-recovery 13/13:"
|
||||
verify_recovery_ok >/dev/null && echo " OK (13/13 MATCH)"
|
||||
echo " [guard B] Host-Tree vs Gold-Manifest (63 MATCH / 0 DIVERGED / 0 MISSING; nur erlaubte Legacy-EXTRA):"
|
||||
verify_host_gold_context || die "Guard B FAIL: Host-Gold-Kontext nicht reproduzierbar (FAIL CLOSED)"
|
||||
echo " [guard A] verify-recovered-gold-build-source (Build-Source 63/63):"
|
||||
verify_recovered_gold_build_source >/dev/null || die "Guard A FAIL: Recovery-Build-Source nicht verifiziert (FAIL CLOSED)"
|
||||
echo " OK (63 MATCH / 0 DIVERGED / 0 MISSING / 0 unerlaubte EXTRA)"
|
||||
echo " [guard B] Recovery-Build-Input (Build-Support SHAs + .dockerignore + Dockerfile + forbidden extras + FINAL_BUILD_INPUT_SHA):"
|
||||
verify_recovery_build_input || die "Guard B FAIL: Recovery-Build-Input nicht reproduzierbar (FAIL CLOSED)"
|
||||
echo " Guard B OK"
|
||||
echo " [guard C] Build-Pollution ausgeschlossen (.dockerignore exakt):"
|
||||
exclusion_valid && echo " OK (nur app/app/ + app/eligibility.py)"
|
||||
echo " [guard C] Host-SoT-Drift (NON-BLOCKING Diagnostic):"
|
||||
local hm hmf d=0 m=0 x=0 mat=0 l=0 goldrel goldsha nrel nsha
|
||||
hmf="$(mktemp)"; gen_host_manifest > "$hmf"
|
||||
while read -r goldsha goldrel; do
|
||||
goldrel="${goldrel#./}"; [ -n "$goldrel" ] || continue
|
||||
l=$((l+1))
|
||||
nsha="$(awk -v r="$goldrel" '$2==r{print $1}' "$hmf")"
|
||||
if [ -z "$nsha" ]; then m=$((m+1));
|
||||
elif [ "$nsha" = "$goldsha" ]; then mat=$((mat+1));
|
||||
else d=$((d+1)); fi
|
||||
done < "$GOLD_MANIFEST"
|
||||
while read -r nsha nrel; do
|
||||
nrel="${nrel#./}"
|
||||
found="$(awk -v r="$nrel" '$2==r{print $1}' "$GOLD_MANIFEST")"
|
||||
[ -z "$found" ] && x=$((x+1))
|
||||
done < "$hmf"
|
||||
rm -f "$hmf"
|
||||
if [ $d -eq 0 ] && [ $m -eq 0 ] && [ $x -eq 0 ]; then
|
||||
echo " HOST_SOT_DRIFT=NONE (MATCH=$mat)"
|
||||
else
|
||||
echo " HOST_SOT_DRIFT=DETECTED (MATCH=$mat DIVERGED=$d MISSING=$m EXTRA=$x) — NON-BLOCKING, Host-SoT wird nach RECOVERY READY repariert"
|
||||
fi
|
||||
echo " [guard D] Dockerfile exakt erwartet:"; dockerfile_expected && echo " OK"
|
||||
echo " [guard E] Gold-Container Running + /health + /health/ready:"
|
||||
ensure_gold_run
|
||||
|
|
@ -705,16 +826,19 @@ verify_image_config() {
|
|||
echo " [config] OK (ENV/secrets NICHT ausgegeben; nur redigierte Struktur)"
|
||||
}
|
||||
|
||||
# ---- build-recovery-gold (NUR Build aus Host-SoT; DRY_RUN = kein Build) ----
|
||||
# ---- build-recovery-gold (NUR Build aus verifizierter Gold Source; DRY_RUN = kein Build) ----
|
||||
build_recovery_gold() {
|
||||
expect_zero "$@"
|
||||
echo "=== BUILD-RECOVERY-GOLD ($RUN_MODE) ==="
|
||||
# Build Context Rotation (Punkt 11): ausschliesslich aus kontrolliertem State.
|
||||
# Kein statisches /opt/historical-v2. Keine User-Argumente.
|
||||
load_recovery_state
|
||||
echo " Build Context : $RECOVERY_BUILD_CONTEXT (aus Control-Plane-State)"
|
||||
echo " Dockerfile : $RECOVERY_BUILD_DOCKERFILE (aus Control-Plane-State)"
|
||||
# Build Context Rotation (Punkt 11): ausschliesslich aus kontrolliertem SOURCE-State.
|
||||
# Kein statisches /opt/historical-v2. Keine User-Argumente. Der IMAGE-State existiert
|
||||
# erst NACH dem Build und ist hier KEIN Input (zirkuläres Gate behoben).
|
||||
load_recovery_source_state
|
||||
echo " Build Context : $RECOVERY_BUILD_CONTEXT (aus Control-Plane-SOURCE-State)"
|
||||
echo " Dockerfile : $RECOVERY_BUILD_DOCKERFILE (aus Control-Plane-SOURCE-State)"
|
||||
echo " Recovery-Tag : $RECOVERY_REBUILD_TAG (fix, direkt getaggt, KEIN latest)"
|
||||
echo " Source-Session: $RECOVERY_GOLD_SOURCE_SESSION"
|
||||
echo " Build-Input : $RECOVERY_GOLD_BUILD_INPUT_SHA"
|
||||
echo " Guards:"
|
||||
# Build Preflight (Punkt 12): verify-recovered-gold-build-source PASS zwingend.
|
||||
echo " [preflight] verify-recovered-gold-build-source:"
|
||||
|
|
@ -724,15 +848,35 @@ build_recovery_gold() {
|
|||
# Build-Context-Manifest (Punkt 5) — read-only RECOVERY_GOLD_BUILD_INPUT
|
||||
echo " [build-input] RECOVERY_GOLD_BUILD_INPUT (Build-Context-Manifest):"
|
||||
build_context_manifest | while IFS= read -r line; do echo " $line"; done
|
||||
# Punkt 7: FINAL BUILD INPUT TOCTOU — unmittelbar vor docker build neu berechnen/verifizieren.
|
||||
echo " [build-input-sha] FINAL_BUILD_INPUT_SHA (TOCTOU, unmittelbar vor Build):"
|
||||
local got_sha
|
||||
got_sha="$( ( cd "$RECOVERY_BUILD_CONTEXT" && 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" ] && sha256sum "$f"; done ) | sha256sum | awk '{print $1}' )"
|
||||
[ "$got_sha" = "$RECOVERY_GOLD_BUILD_INPUT_SHA" ] || die "build-recovery-gold: FINAL_BUILD_INPUT_SHA mismatch (Source veraendert) (FAIL CLOSED)"
|
||||
echo " == $got_sha (OK)"
|
||||
if [ "$RUN_MODE" = "DRY_RUN" ]; then
|
||||
echo " [post-build] wuerde Image-ID ueber festen Tag ermitteln: docker image inspect $RECOVERY_REBUILD_TAG --format '{{.Id}}'"
|
||||
echo " [post-build] wuerde atomar recovery-image.state erzeugen: $RECOVERY_IMAGE_STATE"
|
||||
echo "DRY_RUN_PASS: build-recovery-gold wuerde docker build -f $RECOVERY_BUILD_DOCKERFILE -t $RECOVERY_REBUILD_TAG $RECOVERY_BUILD_CONTEXT (keine Aktion)."
|
||||
exit 0
|
||||
fi
|
||||
mode_or_die
|
||||
echo " [build] docker build -f $RECOVERY_BUILD_DOCKERFILE -t $RECOVERY_REBUILD_TAG $RECOVERY_BUILD_CONTEXT"
|
||||
docker build -f "$RECOVERY_BUILD_DOCKERFILE" -t "$RECOVERY_REBUILD_TAG" "$RECOVERY_BUILD_CONTEXT" || die "build-recovery-gold fehlgeschlagen (FAIL CLOSED)"
|
||||
# Punkt 8: Build-Output-Identity — Image-ID NICHT aus Textausgabe erraten, sondern
|
||||
# kontrolliert ueber den fest verdrahteten Recovery-Tag ermitteln. Keine freie Tag-Eingabe.
|
||||
local newid
|
||||
newid="$(docker image inspect -f '{{.Id}}' "$RECOVERY_REBUILD_TAG" 2>/dev/null)"
|
||||
# Punkt 9: Image-ID-Validierung — vorhanden, sha256:, vollstaendig, inspectable, exakt zum Tag.
|
||||
[[ "$newid" =~ ^sha256:[a-f0-9]{64}$ ]] || die "build-recovery-gold: Image-ID ungueltig/fehlt (FAIL CLOSED)"
|
||||
docker image inspect "sha256:${newid#sha256:}" >/dev/null 2>&1 || die "build-recovery-gold: Image nicht per ID inspectable (FAIL CLOSED)"
|
||||
local tagid
|
||||
tagid="$(docker image inspect -f '{{.Id}}' "$RECOVERY_REBUILD_TAG" 2>/dev/null)"
|
||||
[ "$tagid" = "$newid" ] || die "build-recovery-gold: Tag->ID nicht eindeutig ($tagid != $newid) (FAIL CLOSED)"
|
||||
echo " [post-build] Image-ID (aus festem Tag): $newid"
|
||||
# Punkt 11: State-Write NUR NACH erfolgreichem Build + Tag->ID PASS. Atomar.
|
||||
write_recovery_image_state "$newid"
|
||||
echo " [post-build] recovery-image.state atomar geschrieben: $RECOVERY_IMAGE_STATE"
|
||||
echo "RECOVERY_GOLD_IMAGE_ID=$newid"
|
||||
echo "KLASSIFIZIERUNG: REBUILT RECOVERY GOLD IMAGE (NICHT das Original-Gold-Image; Original f1a6f33e... = UNAVAILABLE)"
|
||||
echo "BUILD-RECOVERY-GOLD OK. KEIN Recreate, KEIN Runner. latest unangetastet."
|
||||
|
|
|
|||
Loading…
Reference in a new issue