Phase 13.5: Image Retention & Archive Control Plane - feste Subcommands archive/pin/verify-archive/verify-pin/verify-ready/dry-run restore (FAIL CLOSED, kein Overwrite, kein Coolify-Cleanup-Disable, kein coolify.managed-Spoof); Restore nur dry-run (real blockiert)
This commit is contained in:
parent
8187a4bc1d
commit
65c1d57fa7
1 changed files with 432 additions and 4 deletions
|
|
@ -187,6 +187,32 @@ RUNNER_REMEDIATION_SHA="6cca3c55bb559d75be2f58e59c231003902f124a54ada25582970524
|
||||||
# Candidate/Recovery duerfen code-identisch sein, aber verschiedene Image-IDs haben (KEIN Fehler).
|
# Candidate/Recovery duerfen code-identisch sein, aber verschiedene Image-IDs haben (KEIN Fehler).
|
||||||
REM_NOOP_IMAGE_ID="sha256:1f3ba210780c5a00d4b73c5ee7817a88671fee2607a4ae0d5b1cefef1a1261c7"
|
REM_NOOP_IMAGE_ID="sha256:1f3ba210780c5a00d4b73c5ee7817a88671fee2607a4ae0d5b1cefef1a1261c7"
|
||||||
|
|
||||||
|
# ------------------------------------------------- IMAGE-RETENTION / ARCHIVE CONTROL PLANE
|
||||||
|
# Phase 13.5 · Schutzschicht gegen bestaetigten Coolify DockerCleanupJob
|
||||||
|
# (taegl. 00:00 UTC, force_docker_cleanup=true, rmi auf getaggte Images ohne
|
||||||
|
# coolify.managed-Label). ZWEI unabhaengige Ebenen je Critical Image:
|
||||||
|
# SCHICHT A = Stopped Pin Container (Image-Referenz via docker create, nie gestartet)
|
||||||
|
# SCHICHT B = Persistentes, sha256-verifiziertes docker save-Archiv
|
||||||
|
# Kein Coolify-Cleanup-Disable. Kein coolify.managed-Spoof. Kein Overwrite.
|
||||||
|
# Fester Archive-Root (Punkt 3) — root:root, 700, KEINE freien Pfade.
|
||||||
|
ARCHIVE_ROOT="/opt/rq-historical-image-archives"
|
||||||
|
# Pin-Container-Namen (Punkt 9) — fest, hardcoded, KEIN freier Name.
|
||||||
|
PIN_RECOVERY_NAME="rq-historical-pin-recovery-gold"
|
||||||
|
PIN_REMEDIATION_NAME="rq-historical-pin-phase13-5-runner-remediation"
|
||||||
|
# Unterordner je Rolle (Punkt 3)
|
||||||
|
declare -A ARCHIVE_SUB=( [recovery]="recovery" [remediation]="candidates" )
|
||||||
|
# Archiv-Session-IDs: kurze Image-ID (<IMAGE_ID_SHORT>), die den immutable
|
||||||
|
# Session-Ordner benennt. Recovery = bekannte Original-ID; Remediation =
|
||||||
|
# FULL_IMAGE_ID aus Build-Session (41e357a1..., aktuell im Store fehlend).
|
||||||
|
RECOVERY_ARCHIVE_ID="${RECOVERY_GOLD_IMAGE_ID#sha256:}"
|
||||||
|
RECOVERY_ARCHIVE_ID="${RECOVERY_ARCHIVE_ID:0:12}"
|
||||||
|
# Remediation-Archiv-Session-ID: kurze ID der Build-Session-Remendation (41e357a1...).
|
||||||
|
REMEDIATION_ARCHIVE_ID="41e357a12b90"
|
||||||
|
# Remediation-Archiv-Referenz (aus Build-Session falls vorhanden, sonst leer):
|
||||||
|
# wird zur Laufzeit per remediation_full_image_id() aufgeloest.
|
||||||
|
# Pin-Referenz-Ziel (Schicht A): exakt diese Image-ID muss der Pin-Container referenzieren.
|
||||||
|
PIN_REMEDIATION_TARGET_ID="" # zur Laufzeit gesetzt
|
||||||
|
|
||||||
# RUN_MODE (Mutation Guard): READ_ONLY | DRY_RUN | MUTATING
|
# RUN_MODE (Mutation Guard): READ_ONLY | DRY_RUN | MUTATING
|
||||||
RUN_MODE=MUTATING
|
RUN_MODE=MUTATING
|
||||||
|
|
||||||
|
|
@ -1406,7 +1432,392 @@ validate_stage() {
|
||||||
# ---- redact: maskiert Passwort-/Secret-Werte (fuer build-def Anzeige) ----
|
# ---- redact: maskiert Passwort-/Secret-Werte (fuer build-def Anzeige) ----
|
||||||
redact_line() {
|
redact_line() {
|
||||||
# Zeilen mit "PASSWORD=" / "SECRET=" / "TOKEN=" : Wert maskieren
|
# Zeilen mit "PASSWORD=" / "SECRET=" / "TOKEN=" : Wert maskieren
|
||||||
echo "$1" | sed -E 's/(PASSWORD|PASSWORDS|SECRET|SECRETS|TOKEN|TOKENS|API_KEY|CLIENT_SECRET|PRIVATE_KEY)(["'"'"']?[[:space:]]*=[[:space:]]*["'"'"']?)[^[:space:]"'"'"']+/\1\2***REDACTED***/Ig'
|
echo "$1" | sed -E 's/(PASSWORD|PASSWORDS|SECRET|SECRETS|TOKEN|TOKENS|API_KEY|CLIENT_SECRET|PRIVATE_KEY)(["'"'"']?[[:space:]]*=[[:space:]]*["'"'"']?)[^[:space:]"'"'"']+/\\1\\2***REDACTED***/Ig'
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# IMAGE-RETENTION / ARCHIVE CONTROL PLANE (Phase 13.5 Schutzschicht)
|
||||||
|
# Zwei unabhängige Ebenen je Critical Image:
|
||||||
|
# A) Stopped Pin Container (docker create, nie gestartet) -> Image-Referenz
|
||||||
|
# B) sha256-verifiziertes docker save-Archiv -> externes Archiv
|
||||||
|
# READY-Gate: IMAGE VERIFY + PIN VERIFY + ARCHIVE VERIFY alle PASS.
|
||||||
|
# KEIN Overwrite. KEIN Coolify-Cleanup-Disable/Managed-Spoof. Kein echter
|
||||||
|
# Rebuild/Restore/Recreate. Kein freies Input (feste Rollen: recovery / remediation).
|
||||||
|
# =============================================================================
|
||||||
|
role_ok() { # $1 = role -> 0 wenn gueltig
|
||||||
|
case "$1" in recovery|remediation) return 0 ;; *) return 1 ;; esac
|
||||||
|
}
|
||||||
|
# Feste Rollen->Image/Identity-Bindung (Punkt 2/4/8/9/10) — hardcoded.
|
||||||
|
role_image_id() { # $1 = role -> echo erwartete FULL_IMAGE_ID
|
||||||
|
case "$1" in
|
||||||
|
recovery) echo "$RECOVERY_GOLD_IMAGE_ID" ;; # sha256:d2db926d...
|
||||||
|
remediation) echo "$REMEDIATION_TAG" ;; # Tag -> aufgeloest via inspect (Image fehlt aktuell)
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
role_tag() {
|
||||||
|
case "$1" in
|
||||||
|
recovery) echo "$RECOVERY_GOLD_TAG_FINAL" ;;
|
||||||
|
remediation) echo "$REMEDIATION_TAG" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
role_pin() {
|
||||||
|
case "$1" in
|
||||||
|
recovery) echo "$PIN_RECOVERY_NAME" ;;
|
||||||
|
remediation) echo "$PIN_REMEDIATION_NAME" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
role_arch_sub() { echo "${ARCHIVE_SUB[$1]}" ; }
|
||||||
|
role_arch_dir() { echo "$ARCHIVE_ROOT/$(role_arch_sub "$1")" ; }
|
||||||
|
|
||||||
|
# --- Schicht A: Stopped Pin Container ---
|
||||||
|
# Container-Referenz schuetzt Image-Objekt vor Coolify rmi (empirisch belegt:
|
||||||
|
# No-op ueberlebte, weil Container 2fe3d30c... es referenzierte).
|
||||||
|
pin_container_identity_ok() { # $1 = role, $2 = actual container id -> 0 wenn ok
|
||||||
|
local role="$1" actual="$2" expected
|
||||||
|
case "$role" in
|
||||||
|
recovery) expected="$RECOVERY_GOLD_IMAGE_ID" ;;
|
||||||
|
remediation) expected="$PIN_REMEDIATION_TARGET_ID" ;;
|
||||||
|
esac
|
||||||
|
[ "$actual" = "$expected" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Schicht B: Archive Verify ---
|
||||||
|
verify_archive() { # $1 = role (read-only)
|
||||||
|
local role="$1" adir sid
|
||||||
|
adir="$(role_arch_dir "$role")"
|
||||||
|
case "$role" in
|
||||||
|
recovery) sid="$RECOVERY_ARCHIVE_ID" ;;
|
||||||
|
remediation) sid="$REMEDIATION_ARCHIVE_ID" ;;
|
||||||
|
esac
|
||||||
|
# Archive existiert
|
||||||
|
[ -d "$adir" ] || die "verify-archive($role): kein Archiv-Root (FAIL CLOSED)"
|
||||||
|
local base="$adir/$sid"
|
||||||
|
[ -d "$base" ] || die "verify-archive($role): kein Session-Ordner $base (FAIL CLOSED)"
|
||||||
|
# Pflichtdateien
|
||||||
|
local f img meta manifest runtime
|
||||||
|
img="$base/image.tar"; meta="$base/session.meta"; manifest="$base/app.manifest"; runtime="$base/runtime-config.txt"
|
||||||
|
[ -f "$img" ] && [ ! -L "$img" ] || die "verify-archive($role): image.tar fehlt/kein File (FAIL CLOSED)"
|
||||||
|
[ -s "$img" ] || die "verify-archive($role): image.tar leer (FAIL CLOSED)"
|
||||||
|
for f in "$meta" "$manifest" "$runtime"; do
|
||||||
|
[ -f "$f" ] && [ ! -L "$f" ] || die "verify-archive($role): fehlt $(basename "$f") (FAIL CLOSED)"
|
||||||
|
done
|
||||||
|
[ "$(stat -c '%U' "$img")" = "root" ] || die "verify-archive($role): image.tar Owner != root (FAIL CLOSED)"
|
||||||
|
case "$(stat -c '%a' "$img")" in 600|640|700|400) ;; *) die "verify-archive($role): image.tar Mode unsicher ($(stat -c '%a' "$img")) (FAIL CLOSED)" ;; esac
|
||||||
|
# Sha256 stimmt (Punkt 7)
|
||||||
|
local want got
|
||||||
|
want="$(awk -F'=' '$1=="ARCHIVE_SHA256"{print $2}' "$meta" 2>/dev/null)"
|
||||||
|
[ -n "$want" ] && [[ "$want" =~ ^[a-f0-9]{64}$ ]] || die "verify-archive($role): ARCHIVE_SHA256 fehlt/ungueltig in session.meta (FAIL CLOSED)"
|
||||||
|
got="$(sha256sum "$img" | awk '{print $1}')"
|
||||||
|
[ "$got" = "$want" ] || die "verify-archive($role): image.tar SHA mismatch (FAIL CLOSED)"
|
||||||
|
# session.meta Vollstaendigkeit (Punkt 5)
|
||||||
|
local key
|
||||||
|
for key in ROLE TAG FULL_IMAGE_ID ARCHIVE_SHA256 ARCHIVE_SIZE CREATED_AT HOST_SOT_RUNNER_SHA BUILD_INPUT_SHA WRAPPER_SHA APP_MANIFEST_SHA RUNTIME_CONFIG_SHA; do
|
||||||
|
grep -q "^$key=.*" "$meta" || die "verify-archive($role): session.meta fehlt $key (FAIL CLOSED)"
|
||||||
|
done
|
||||||
|
local mrole
|
||||||
|
mrole="$(awk -F'=' '$1=="ROLE"{print $2}' "$meta" 2>/dev/null)"
|
||||||
|
[ "$mrole" = "$role" ] || die "verify-archive($role): ROLE in session.meta = '$mrole' (FAIL CLOSED)"
|
||||||
|
echo "VERIFY-ARCHIVE($role): PASS ($base)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ready-Gate (Punkt 14) ---
|
||||||
|
verify_ready() { # $1 = role
|
||||||
|
local role="$1" label
|
||||||
|
case "$role" in
|
||||||
|
recovery) label="RECOVERY READY" ;;
|
||||||
|
remediation) label="CANDIDATE READY" ;;
|
||||||
|
esac
|
||||||
|
echo "=== $label (READY-Gate: IMAGE VERIFY + PIN VERIFY + ARCHIVE VERIFY) ==="
|
||||||
|
# IMAGE VERIFY (zwingend inspectable + korrekt)
|
||||||
|
echo " [IMAGE] $(role_tag "$role"):"
|
||||||
|
verify_image_present "$role" || die "READY-Gate FAIL: Image nicht inspectable (FAIL CLOSED)"
|
||||||
|
echo " OK"
|
||||||
|
echo " [PIN] $(role_pin "$role"):"
|
||||||
|
verify_pin "$role" || die "READY-Gate FAIL: Pin nicht PASS (FAIL CLOSED)"
|
||||||
|
echo " OK"
|
||||||
|
echo " [ARCHIVE] $(role_arch_dir "$role"):"
|
||||||
|
verify_archive "$role" >/dev/null || die "READY-Gate FAIL: Archive nicht PASS (FAIL CLOSED)"
|
||||||
|
echo " OK"
|
||||||
|
echo "$label: PASS"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Image-Present-Verify (Schicht-Pruefung: Image inspectable + Identity) ---
|
||||||
|
verify_image_present() { # $1 = role (read-only) -> 0 wenn Image vorhanden+bindung ok
|
||||||
|
local role="$1" id tag actualid
|
||||||
|
tag="$(role_tag "$role")"
|
||||||
|
case "$role" in
|
||||||
|
recovery)
|
||||||
|
id="$RECOVERY_GOLD_IMAGE_ID"
|
||||||
|
# Tag muss exakt auf die immutable Recovery-ID zeigen (kein Overwrite-Erwartung)
|
||||||
|
actualid="$(docker image inspect -f '{{.Id}}' "$tag" 2>/dev/null || true)"
|
||||||
|
[ "$actualid" = "$id" ] || return 1
|
||||||
|
;;
|
||||||
|
remediation)
|
||||||
|
# Candidate: FULL_IMAGE_ID aus Build-Session; Tag -> ID Bindung
|
||||||
|
if ! remediation_session; then die "verify-image(remediation): keine gueltige Build-Session (FAIL CLOSED)"; fi
|
||||||
|
id="$(remediation_full_image_id)"
|
||||||
|
actualid="$(docker image inspect -f '{{.Id}}' "$id" 2>/dev/null || true)"
|
||||||
|
[ -n "$actualid" ] || return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Schicht A: Pin-Verify (Punkt 12) ---
|
||||||
|
verify_pin() { # $1 = role (read-only) -> 0 wenn Pin PASS
|
||||||
|
local role="$1" name imgid nw mounts ports rp priv capadd expected state
|
||||||
|
name="$(role_pin "$role")"
|
||||||
|
docker container inspect "$name" >/dev/null 2>&1 || { echo " PIN($role): Container $name fehlt"; return 1; }
|
||||||
|
expected="$(role_pin_target_id "$role")"
|
||||||
|
imgid="$(docker container inspect -f '{{.Image}}' "$name" 2>/dev/null)"
|
||||||
|
# .Image ist die Image-ID des referenzierten Images (exakt FULL_IMAGE_ID)
|
||||||
|
[ -n "$expected" ] || die "verify-pin($role): Ziel-Image-ID nicht bekannt (FAIL CLOSED)"
|
||||||
|
# Image-ID kann lang (sha256:...) sein — Bindung: .Image == erwartete FULL_IMAGE_ID
|
||||||
|
[ "$imgid" = "$expected" ] || { echo " PIN($role): Image-ID $imgid != erwartet $expected"; return 1; }
|
||||||
|
state="$(docker container inspect -f '{{.State.Status}}' "$name" 2>/dev/null)"
|
||||||
|
case "$state" in created|exited|stopped) ;; *) echo " PIN($role): State=$state (nicht created/stopped — VERBOTEN running)"; return 1 ;; esac
|
||||||
|
nw="$(docker container inspect -f '{{.HostConfig.NetworkMode}}' "$name" 2>/dev/null)"
|
||||||
|
[ "$nw" = "none" ] || { echo " PIN($role): NetworkMode=$nw != none"; return 1; }
|
||||||
|
mounts="$(docker container inspect -f '{{json .Mounts}}' "$name" 2>/dev/null)"
|
||||||
|
[ "$mounts" = "[]" ] || { echo " PIN($role): Mounts != []"; return 1; }
|
||||||
|
ports="$(docker container inspect -f '{{json .HostConfig.PortBindings}}' "$name" 2>/dev/null)"
|
||||||
|
[ "$ports" = "{}" ] || { echo " PIN($role): PortBindings != {}"; return 1; }
|
||||||
|
rp="$(docker container inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$name" 2>/dev/null)"
|
||||||
|
[ "$rp" = "no" ] || { echo " PIN($role): RestartPolicy=$rp != no"; return 1; }
|
||||||
|
priv="$(docker container inspect -f '{{.HostConfig.Privileged}}' "$name" 2>/dev/null)"
|
||||||
|
[ "$priv" = "false" ] || { echo " PIN($role): Privileged=$priv"; return 1; }
|
||||||
|
capadd="$(docker container inspect -f '{{json .HostConfig.CapAdd}}' "$name" 2>/dev/null)"
|
||||||
|
[ "$capadd" = "null" ] || [ "$capadd" = "[]" ] || { echo " PIN($role): CapAdd=$capadd"; return 1; }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ziel-Image-ID fuer Pin (Schicht A bindet exakt FULL_IMAGE_ID) ---
|
||||||
|
role_pin_target_id() { # $1 = role
|
||||||
|
case "$1" in
|
||||||
|
recovery) echo "$RECOVERY_GOLD_IMAGE_ID" ;;
|
||||||
|
remediation)
|
||||||
|
if remediation_session; then echo "$(remediation_full_image_id)"; else echo ""; fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Archive-Metadaten-Generator (Punkt 5; kein Secret) ---
|
||||||
|
gen_archive_meta() { # $1 = role, $2 = session dir, $3 = image id, $4 = archive size, $5 = archive sha
|
||||||
|
local role="$1" dir="$2" image_id="$3" asize="$4" asha="$5" now
|
||||||
|
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
printf 'ROLE=%s\nTAG=%s\nFULL_IMAGE_ID=%s\nARCHIVE_SHA256=%s\nARCHIVE_SIZE=%s\nCREATED_AT=%s\nHOST_SOT_RUNNER_SHA=%s\nBUILD_INPUT_SHA=%s\nWRAPPER_SHA=%s\nAPP_MANIFEST_SHA=%s\nRUNTIME_CONFIG_SHA=%s\n' \
|
||||||
|
"$role" "$(role_tag "$role")" "$image_id" "$asha" "$asize" "$now" \
|
||||||
|
"$HOST_SOT_RUNNER_SHA_CUR" "$BUILD_INPUT_SHA_CUR" "$WRAPPER_SHA_CUR" "$APP_MANIFEST_SHA_CUR" "$RUNTIME_CONFIG_SHA_CUR" \
|
||||||
|
> "$dir/session.meta"
|
||||||
|
chown root:root "$dir/session.meta"; chmod 600 "$dir/session.meta"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- runtime-config Evidence (Punkt 4: runtime-config.txt) — keine Secrets ---
|
||||||
|
write_runtime_config() { # $1 = role, $2 = session dir
|
||||||
|
local role="$1" dir="$2" tag="$(role_tag "$role")" id
|
||||||
|
id="$(docker image inspect -f '{{.Id}}' "$tag" 2>/dev/null)"
|
||||||
|
{
|
||||||
|
echo "RUNTIME CONFIG EVIDENCE (Phase 13.5 Archive) — keine Secrets"
|
||||||
|
echo "ROLE=$role"
|
||||||
|
echo "FULL_IMAGE_ID=$id"
|
||||||
|
echo "Entrypoint=$(docker image inspect -f '{{json .Config.Entrypoint}}' "$tag" 2>/dev/null)"
|
||||||
|
echo "Cmd=$(docker image inspect -f '{{json .Config.Cmd}}' "$tag" 2>/dev/null)"
|
||||||
|
echo "WorkingDir=$(docker image inspect -f '{{.Config.WorkingDir}}' "$tag" 2>/dev/null)"
|
||||||
|
echo "User=$(docker image inspect -f '{{.Config.User}}' "$tag" 2>/dev/null)"
|
||||||
|
echo "ExposedPorts=$(docker image inspect -f '{{json .Config.ExposedPorts}}' "$tag" 2>/dev/null)"
|
||||||
|
echo "ENV_KEYS=$(docker image inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$tag" 2>/dev/null | cut -d= -f1 | tr '\n' ',' | sed 's/,$//')"
|
||||||
|
} > "$dir/runtime-config.txt"
|
||||||
|
chown root:root "$dir/runtime-config.txt"; chmod 600 "$dir/runtime-config.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- app.manifest Evidence (Punkt 4) — Sha256 aller *.py im Image-Layer ---
|
||||||
|
write_app_manifest() { # $1 = role, $2 = session dir, $3 = image id/tag
|
||||||
|
local role="$1" dir="$2" ref="$3" tmpc tmpd
|
||||||
|
tmpc="rq-arch-manifest-$$"
|
||||||
|
tmpd="/tmp/rq-arch-manifest-$$"
|
||||||
|
rm -rf "$tmpd"; mkdir -p "$tmpd"
|
||||||
|
docker create --name "$tmpc" --entrypoint /bin/true "$ref" >/dev/null 2>&1 \
|
||||||
|
|| { rm -rf "$tmpd"; die "archive($role): Temp-Container Fehler (FAIL CLOSED)"; }
|
||||||
|
if ! docker cp "$tmpc:/app/app" "$tmpd/appapp" >/dev/null 2>&1; then
|
||||||
|
docker rm -f "$tmpc" >/dev/null 2>&1 || true; rm -rf "$tmpd"
|
||||||
|
die "archive($role): /app/app nicht lesbar (FAIL CLOSED)"
|
||||||
|
fi
|
||||||
|
docker rm -f "$tmpc" >/dev/null 2>&1 || true
|
||||||
|
( cd "$tmpd/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}' ) > "$dir/app.manifest"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
chown root:root "$dir/app.manifest"; chmod 600 "$dir/app.manifest"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# COMMAND-FUNKTIONEN (Phase 13.5 Archive/Pin) — fail-closed, kein Overwrite,
|
||||||
|
# kein echter Rebuild/Restore/Recreate in dieser Mission.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# archive-recovery-image / archive-remediation-image
|
||||||
|
archive_image() { # $1 = role
|
||||||
|
local role="$1"
|
||||||
|
expect_zero "${@:2}"
|
||||||
|
echo "=== ARCHIVE-$role ($RUN_MODE) ==="
|
||||||
|
local tag id subdir sdir
|
||||||
|
tag="$(role_tag "$role")"
|
||||||
|
case "$role" in
|
||||||
|
recovery) id="$RECOVERY_GOLD_IMAGE_ID" ;;
|
||||||
|
remediation)
|
||||||
|
remediation_session || die "archive(remediation): keine gueltige Build-Session (FAIL CLOSED)"
|
||||||
|
id="$(remediation_full_image_id)"
|
||||||
|
PIN_REMEDIATION_TARGET_ID="$id"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo " FULL_IMAGE_ID=$id"
|
||||||
|
echo " Tag=$tag"
|
||||||
|
# Punkt 6: Image-inspect + Tag->ID Gate VOR save
|
||||||
|
echo " [inspect] Tag -> ID Gate:"
|
||||||
|
local tagid
|
||||||
|
tagid="$(docker image inspect -f '{{.Id}}' "$tag" 2>/dev/null || true)"
|
||||||
|
if [ -z "$tagid" ]; then
|
||||||
|
die "archive($role): Source Image nicht inspectable (SOURCE IMAGE NOT INSPECTABLE — FAIL CLOSED)"
|
||||||
|
fi
|
||||||
|
[ "$tagid" = "$id" ] || die "archive($role): Tag->ID Gate FAIL ($tagid != $id) (FAIL CLOSED)"
|
||||||
|
echo " OK ($tagid)"
|
||||||
|
# Punkt 7: Immutability — Archiv-Session darf NICHT existieren
|
||||||
|
local sdir
|
||||||
|
sdir="$(role_arch_dir "$role")" # = $ARCHIVE_ROOT/<recovery|candidates>
|
||||||
|
local sid
|
||||||
|
case "$role" in
|
||||||
|
recovery) sid="$RECOVERY_ARCHIVE_ID" ;;
|
||||||
|
remediation) sid="${id#sha256:}"; sid="${sid:0:12}" ;;
|
||||||
|
esac
|
||||||
|
local base="$sdir/$sid"
|
||||||
|
# Fail-closed wenn bereits existiert (kein Overwrite)
|
||||||
|
if [ -e "$base" ]; then die "archive($role): Session-Ordner existiert bereits $base (KEIN Overwrite, FAIL CLOSED)"; fi
|
||||||
|
if [ "$RUN_MODE" = "DRY_RUN" ]; then
|
||||||
|
echo "DRY_RUN_PASS: archive($role) wuerde anlegen: $base/ (image.tar + sha256 + session.meta + app.manifest + runtime-config.txt). Keine Aktion."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
mode_or_die
|
||||||
|
# MUTATION: Archive anlegen + docker save
|
||||||
|
mkdir -p "$base" || die "archive($role): Session-Ordner nicht anlegbar (FAIL CLOSED)"
|
||||||
|
# Session.meta ZUSAMMENBAUEN VOR save (fuer Size/SHA brauchen aber erst nach save)
|
||||||
|
# Reihenfolge: save -> size/sha -> meta -> manifest -> runtime
|
||||||
|
echo " [save] docker image save $id > image.tar"
|
||||||
|
if ! docker image save "$id" > "$base/image.tar" 2>/dev/null; then
|
||||||
|
rm -rf "$base"; die "archive($role): docker save fehlgeschlagen (FAIL CLOSED)"
|
||||||
|
fi
|
||||||
|
chown root:root "$base/image.tar"; chmod 600 "$base/image.tar"
|
||||||
|
local asha asize
|
||||||
|
asha="$(sha256sum "$base/image.tar" | awk '{print $1}')"
|
||||||
|
asize="$(stat -c '%s' "$base/image.tar")"
|
||||||
|
# SHAs der Metadaten (zur Laufzeit, aus Build-Session / Host-SoT)
|
||||||
|
HOST_SOT_RUNNER_SHA_CUR="$(sha256sum /opt/historical-v2/app/run_backfill_year.py 2>/dev/null | awk '{print $1}')"
|
||||||
|
BUILD_INPUT_SHA_CUR="$(build_context_manifest 2>/dev/null | sha256sum | awk '{print $1}')"
|
||||||
|
WRAPPER_SHA_CUR="$(sha256sum /usr/local/bin/rq-historical | awk '{print $1}')"
|
||||||
|
APP_MANIFEST_SHA_CUR="$(sha256sum "$base/app.manifest" 2>/dev/null | awk '{print $1}')"
|
||||||
|
RUNTIME_CONFIG_SHA_CUR="$(sha256sum "$base/runtime-config.txt" 2>/dev/null | awk '{print $1}')"
|
||||||
|
gen_archive_meta "$role" "$base" "$id" "$asize" "$asha"
|
||||||
|
write_runtime_config "$role" "$base"
|
||||||
|
write_app_manifest "$role" "$base" "$id"
|
||||||
|
# SHA erneut nach manifest/runtime (weil meta SHA referenziert)
|
||||||
|
APP_MANIFEST_SHA_CUR="$(sha256sum "$base/app.manifest" | awk '{print $1}')"
|
||||||
|
RUNTIME_CONFIG_SHA_CUR="$(sha256sum "$base/runtime-config.txt" | awk '{print $1}')"
|
||||||
|
gen_archive_meta "$role" "$base" "$id" "$asize" "$asha"
|
||||||
|
echo "ARCHIVE($role) OK: $base/ (${asize} bytes, sha256 ${asha:0:16}...)"
|
||||||
|
echo " image.tar.sha256:"
|
||||||
|
sha256sum "$base/image.tar" > "$base/image.tar.sha256"
|
||||||
|
chown root:root "$base/image.tar.sha256"; chmod 600 "$base/image.tar.sha256"
|
||||||
|
}
|
||||||
|
|
||||||
|
# pin-recovery-image / pin-remediation-image
|
||||||
|
pin_image() { # $1 = role
|
||||||
|
local role="$1"
|
||||||
|
expect_zero "${@:2}"
|
||||||
|
echo "=== PIN-$role ($RUN_MODE) ==="
|
||||||
|
local name tag id
|
||||||
|
name="$(role_pin "$role")"
|
||||||
|
tag="$(role_tag "$role")"
|
||||||
|
case "$role" in
|
||||||
|
recovery) id="$RECOVERY_GOLD_IMAGE_ID" ;;
|
||||||
|
remediation)
|
||||||
|
remediation_session || die "pin(remediation): keine gueltige Build-Session (FAIL CLOSED)"
|
||||||
|
id="$(remediation_full_image_id)"
|
||||||
|
PIN_REMEDIATION_TARGET_ID="$id"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
# Punkt 11: Containername darf NICHT existieren (kein Delete/Overwrite/Replace)
|
||||||
|
if docker container inspect "$name" >/dev/null 2>&1; then
|
||||||
|
# Identitaet pruefen; wenn korrekt -> ok (keine Aenderung); wenn falsch -> FAIL CLOSED
|
||||||
|
local existing
|
||||||
|
existing="$(docker container inspect -f '{{.Image}}' "$name" 2>/dev/null)"
|
||||||
|
if pin_container_identity_ok "$role" "$existing"; then
|
||||||
|
echo "PIN($role): Container existiert bereits korrekt (Image=$existing). KEINE Aenderung."
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
die "pin($role): Container $name existiert mit falschem Image ($existing). Kein Replace (FAIL CLOSED)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo " FULL_IMAGE_ID=$id"
|
||||||
|
# Punkt 6: Image muss inspectable sein (Source Gate)
|
||||||
|
if ! docker image inspect "$id" >/dev/null 2>&1; then
|
||||||
|
die "pin($role): Source Image nicht inspectable (SOURCE IMAGE NOT INSPECTABLE — FAIL CLOSED)"
|
||||||
|
fi
|
||||||
|
if [ "$RUN_MODE" = "DRY_RUN" ]; then
|
||||||
|
echo "DRY_RUN_PASS: pin($role) wuerde docker create --name $name --network none --restart no $id (keine Aktion)."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
mode_or_die
|
||||||
|
# MUTATION: minimaler Stopped-Container (Punkt 8/10)
|
||||||
|
echo " [create] docker create --name $name --network none --restart no $id"
|
||||||
|
if ! docker create --name "$name" --network none --restart no "$id" >/dev/null 2>&1; then
|
||||||
|
die "pin($role): docker create fehlgeschlagen (FAIL CLOSED)"
|
||||||
|
fi
|
||||||
|
echo "PIN($role) OK: $name -> $id (Stopped, Network=none, Restart=no)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# verify-recovery-pin / verify-remediation-pin / verify-...-archive / verify-...-ready
|
||||||
|
verify_pin_cmd() { # $1 = role (read-only)
|
||||||
|
local role="$1"; expect_zero "${@:2}"
|
||||||
|
echo "=== VERIFY-PIN($role) ==="
|
||||||
|
if verify_pin "$role"; then echo "VERIFY-PIN($role): PASS"; exit 0
|
||||||
|
else die "VERIFY-PIN($role): FAIL (FAIL CLOSED)"; fi
|
||||||
|
}
|
||||||
|
verify_archive_cmd() { # $1 = role (read-only)
|
||||||
|
local role="$1"; expect_zero "${@:2}"
|
||||||
|
echo "=== VERIFY-ARCHIVE($role) ==="
|
||||||
|
verify_archive "$role" || die "VERIFY-ARCHIVE($role): FAIL (FAIL CLOSED)"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
verify_ready_cmd() { # $1 = role (read-only)
|
||||||
|
local role="$1"; expect_zero "${@:2}"
|
||||||
|
verify_ready "$role" || die "$(case $role in recovery) echo 'RECOVERY READY';; remediation) echo 'CANDIDATE READY';; esac): FAIL (FAIL CLOSED)"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# restore-recovery-image-archive (DESIGN; realer Restore NICHT in dieser Mission ausfuehren)
|
||||||
|
restore_recovery_dry() { # dry-run
|
||||||
|
expect_zero "$@"
|
||||||
|
echo "=== RESTORE-RECOVERY-IMAGE-ARCHIVE (DRY-RUN — DESIGN; kein echter Restore) ==="
|
||||||
|
local sdir sid base img
|
||||||
|
sdir="$(role_arch_dir recovery)"
|
||||||
|
sid="$RECOVERY_ARCHIVE_ID"
|
||||||
|
base="$sdir/$sid"
|
||||||
|
[ -d "$base" ] || die "restore-dry: Archiv-Session fehlt $base (FAIL CLOSED)"
|
||||||
|
echo " 1. Archiv SHA pruefen: $(sha256sum "$base/image.tar" | awk '{print $1}')"
|
||||||
|
echo " 2. Metadaten pruefen: session.meta vorhanden"
|
||||||
|
echo " 3. docker load (in realer Mission; hier NUR Dry-Run)"
|
||||||
|
echo " 4. Resulting Image inspect"
|
||||||
|
echo " 5. Application Manifest pruefen"
|
||||||
|
echo " 6. Runtime Config pruefen"
|
||||||
|
echo " 7. Tag/Identity pruefen"
|
||||||
|
echo " 8. danach Pin Verify"
|
||||||
|
echo " HINWEIS (Punkt 15): Wenn neu geladene FULL_IMAGE_ID != Original-ID bei gleicher App-Identitaet -> NICHT automatisch identisch; sauber Docker-Image-Identity vs Application/Runtime-Equivalence unterscheiden."
|
||||||
|
echo "RESTORE-RECOVERY-DRY-RUN: DESIGN OK (kein echter Restore ausgefuehrt)"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# restore-recovery-image-archive (real — in dieser Mission NICHT ausfuehren; nur definiert)
|
||||||
|
restore_recovery_real() {
|
||||||
|
expect_zero "$@"
|
||||||
|
die "restore-recovery-image-archive: Realer Restore ist in dieser Mission NICHT erlaubt (HARTER STOPP Punkt 30). NUR dry-run (FAIL CLOSED)"
|
||||||
}
|
}
|
||||||
|
|
||||||
SUBCMD="${1:-}"
|
SUBCMD="${1:-}"
|
||||||
|
|
@ -1418,8 +1829,8 @@ if [ "$SUBCMD" = "dry-run" ]; then
|
||||||
SUBCMD="${1:-}"
|
SUBCMD="${1:-}"
|
||||||
shift || true
|
shift || true
|
||||||
case "$SUBCMD" in
|
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|build-runner-remediation|recreate-runner-remediation-safe) ;;
|
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|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image) ;;
|
||||||
*) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|build-runner-remediation|recreate-runner-remediation-safe zulaessig" ;;
|
*) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|build-runner-remediation|recreate-runner-remediation-safe|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image zulaessig" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -1819,6 +2230,23 @@ case "$SUBCMD" in
|
||||||
recreate-historical) recreate_historical "$@" ;;
|
recreate-historical) recreate_historical "$@" ;;
|
||||||
verify-gold-reproduction) verify_gold_reproduction "$@" ;;
|
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-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"
|
# ============ IMAGE-RETENTION / ARCHIVE CONTROL PLANE (Phase 13.5) ============
|
||||||
|
# Schicht B (Archiv, docker save; fail-closed, kein Overwrite) & Schicht A (Pin).
|
||||||
|
# Alle verify-* sind read-only. Realer Restore ist in dieser Mission BLOCKIERT
|
||||||
|
# (nur dry-run). Kein Coolify-Cleanup-Disable, kein coolify.managed-Spoof.
|
||||||
|
archive-recovery-image) archive_image recovery "$@" ;;
|
||||||
|
archive-remediation-image) archive_image remediation "$@" ;;
|
||||||
|
pin-recovery-image) pin_image recovery "$@" ;;
|
||||||
|
pin-remediation-image) pin_image remediation "$@" ;;
|
||||||
|
verify-recovery-archive) verify_archive_cmd recovery "$@" ;;
|
||||||
|
verify-remediation-archive) verify_archive_cmd remediation "$@" ;;
|
||||||
|
verify-recovery-pin) verify_pin_cmd recovery "$@" ;;
|
||||||
|
verify-remediation-pin) verify_pin_cmd remediation "$@" ;;
|
||||||
|
verify-recovery-ready) verify_ready_cmd recovery "$@" ;;
|
||||||
|
verify-remediation-ready) verify_ready_cmd remediation "$@" ;;
|
||||||
|
restore-recovery-image-archive) restore_recovery_real "$@" ;;
|
||||||
|
dry-restore-recovery-image-archive) restore_recovery_dry "$@" ;;
|
||||||
|
|
||||||
|
*) 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|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image|verify-recovery-archive|verify-remediation-archive|verify-recovery-pin|verify-remediation-pin|verify-recovery-ready|verify-remediation-ready|dry-restore-recovery-image-archive|restore-recovery-image-archive"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue