rq-historical: SOT-Discovery read-only Kanal (build-def, build-files, host/container-manifest, host/container-code, tree-diff) — redacted Secrets
This commit is contained in:
parent
6324c8aaac
commit
9129e76a6d
1 changed files with 154 additions and 79 deletions
|
|
@ -1,43 +1,36 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# rq-historical — eng begrenzter root-Wrapper fuer Red Queen (rq-host)
|
# rq-historical — eng begrenzter root-Wrapper fuer Red Queen (rq-host)
|
||||||
# RED-QUEEN-VPS-HOST-ACCESS + PHASE-13.5-READONLY + DEV/TEST/DEPLOY-CHANNEL
|
# RED-QUEEN-VPS-HOST-ACCESS + READONLY-CODE + DEV/TEST/DEPLOY + SOT-DISCOVERY
|
||||||
# | Rain Ocampo (Hermes) | 2026-08-29
|
# | Rain Ocampo (Hermes) | 2026-08-29
|
||||||
#
|
#
|
||||||
# LEAST-PRIVILEGE-Design:
|
# LEAST-PRIVILEGE-Design:
|
||||||
# - rq-host hat KEINE docker-Gruppe, KEINEN Docker-Socket, KEIN sudo ALL,
|
# - rq-host hat KEINE docker-Gruppe, KEINEN Docker-Socket, KEIN sudo ALL,
|
||||||
# KEINE Root-Shell. Einziger Root-Kanal: NOPASSWD:/usr/local/bin/rq-historical
|
# KEINE Root-Shell. Einziger Root-Kanal: NOPASSWD:/usr/local/bin/rq-historical
|
||||||
# - Dieser Wrapper ist der EINZIGE Kanal zu historical-service. Er laeuft als
|
# - Dieser Wrapper ist der EINZIGE Kanal zu historical-service & Host-SoT.
|
||||||
# root (via sudoers), erzwingt aber fuer Dev/Test/Deploy strikte Fixierung:
|
# Keine freien Pfade, kein arbitrary cat/find/grep, kein Shell.
|
||||||
# * feste Staging-Roots, feste Allowlist (KEY->Staging-Datei->Containerpfad)
|
|
||||||
# * keine freien Pfade, keine ../, kein Symlink-Escape, kein Mount-Arg
|
|
||||||
# * automatisches Backup + Healthcheck + Hash-Reverify vor/nach Deploy
|
|
||||||
# * deploy != execution: nie automatischer Backfill/Runner/Watchdog
|
|
||||||
# - Deploy-Methode: atomic docker cp der validierten Stage-Datei in den
|
|
||||||
# laufenden Container (Container ist beschreibbar, ReadonlyRootfs=false).
|
|
||||||
# Rebuild via compose-build NICHT freigegeben (wuerde rq-host Docker-Build
|
|
||||||
# + Container-Recreation geben -> Punkt 2/10-Verletzung).
|
|
||||||
# - Test: py_compile der Stage-Kopie in Container-/tmp (keine Mutation des
|
|
||||||
# laufenden Produktivcodes).
|
|
||||||
#
|
#
|
||||||
# SUBKOMMANDOS:
|
# SoT-DISCOVERY (read-only, NO copy/rebuild/deploy/fix):
|
||||||
# ps | inspect | logs [--tail N] | run [--start --end --limit-days] (bestehend)
|
# build-def -> zeigt Dockerfile + Compose-Builddefinition (redacted)
|
||||||
# code <KEY> | hashes | code-list (bestehend, read-only)
|
# build-files -> Dockerfile + compose + .dockerignore Auflistung
|
||||||
# stage-dir (zeigt Staging-Pfade)
|
# host-manifest -> SHA256+size aller *.py unter /opt/historical-v2/app
|
||||||
# stage-check <KEY> (validiert Staging-Kandidat)
|
# host-code <rel> -> gezielter Code-Read NUR aus host-manifest (kontrolliert)
|
||||||
# test <KEY> (py_compile in /tmp, isoliert)
|
# container-manifest -> SHA256+size aller *.py unter historical-service:/app/app
|
||||||
# search-writes (fixe Write-Schreibpfad-Suche)
|
# container-code <rel> -> gezielter Code-Read NUR aus container-manifest
|
||||||
# healthcheck (/health + /health/ready 200)
|
# tree-diff -> Host vs Container Manifest-Diff
|
||||||
# backup <KEY> (aktuellen Container-Code sichern)
|
#
|
||||||
# deploy <KEY> (Backup->stage-check->test->cp->reverify->health)
|
# SUBKOMMANDOS (bestehend, unveraendert):
|
||||||
# rollback <KEY> (zurueck zum letzten Backup)
|
# ps | inspect | logs [--tail N] | run [..] | code <KEY> | hashes | code-list
|
||||||
|
# stage-dir | stage-check <KEY> | test <KEY> | search-writes | healthcheck
|
||||||
|
# backup <KEY> | deploy <KEY> | rollback <KEY>
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
CONTAINER=historical-service
|
CONTAINER=historical-service
|
||||||
|
HOST_APP=/opt/historical-v2/app
|
||||||
|
HOST_DIR=/opt/historical-v2
|
||||||
|
|
||||||
# --------------------------------------------------------- Allowlist (fest)
|
# --------------------------------------------------------- Allowlist (fest)
|
||||||
# KEY -> Container-Zielpfad (identisch mit 'code'-Read-Allowlist)
|
|
||||||
declare -A TARGET_PATHS=(
|
declare -A TARGET_PATHS=(
|
||||||
[runner]="/app/app/run_backfill_year.py"
|
[runner]="/app/app/run_backfill_year.py"
|
||||||
[backfill]="/app/app/backfill.py"
|
[backfill]="/app/app/backfill.py"
|
||||||
|
|
@ -48,40 +41,28 @@ declare -A TARGET_PATHS=(
|
||||||
[sanity]="/app/app/trust/sanity.py"
|
[sanity]="/app/app/trust/sanity.py"
|
||||||
[trust_gate]="/app/app/trust/trust_gate.py"
|
[trust_gate]="/app/app/trust/trust_gate.py"
|
||||||
)
|
)
|
||||||
|
|
||||||
# KEY -> Staging-Dateiname (fest, NUR Dateiname im Staging-Root)
|
|
||||||
declare -A STAGE_FILES=(
|
declare -A STAGE_FILES=(
|
||||||
[runner]="run_backfill_year.py"
|
[runner]="run_backfill_year.py" [backfill]="backfill.py" [pipeline]="pipeline_v2.py"
|
||||||
[backfill]="backfill.py"
|
[repository]="repository.py" [provenance]="provenance.py" [trust]="trust.py"
|
||||||
[pipeline]="pipeline_v2.py"
|
[sanity]="sanity.py" [trust_gate]="trust_gate.py"
|
||||||
[repository]="repository.py"
|
|
||||||
[provenance]="provenance.py"
|
|
||||||
[trust]="trust.py"
|
|
||||||
[sanity]="sanity.py"
|
|
||||||
[trust_gate]="trust_gate.py"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
STAGING_ROOT=/opt/rq-historical-staging
|
STAGING_ROOT=/opt/rq-historical-staging
|
||||||
BACKUP_ROOT=/opt/rq-historical-backups
|
BACKUP_ROOT=/opt/rq-historical-backups
|
||||||
|
|
||||||
# Relevanter Pfad fuer den 'run'-Subbefehl (Backfill-Entrypoint)
|
|
||||||
SCRIPT=/app/app/run_backfill_year.py
|
SCRIPT=/app/app/run_backfill_year.py
|
||||||
|
|
||||||
# ------------------------------------------------------------ Validierung
|
|
||||||
die() { echo "ERR: $*" >&2; exit 2; }
|
die() { echo "ERR: $*" >&2; exit 2; }
|
||||||
req_ok() {
|
req_ok() {
|
||||||
docker inspect -f '{{.State.Running}}' "$CONTAINER" >/dev/null 2>&1 \
|
docker inspect -f '{{.State.Running}}' "$CONTAINER" >/dev/null 2>&1 \
|
||||||
|| die "Container '$CONTAINER' nicht vorhanden oder nicht am Laufen"
|
|| die "Container '$CONTAINER' nicht vorhanden oder nicht am Laufen"
|
||||||
}
|
}
|
||||||
key_path() { echo "${TARGET_PATHS[$1]:-}"; } # $1=key -> Containerpfad
|
key_path() { echo "${TARGET_PATHS[$1]:-}"; }
|
||||||
stage_file() { echo "$STAGING_ROOT/${STAGE_FILES[$1]:-}"; } # $1=key -> abs Staging-Pfad
|
stage_file() { echo "$STAGING_ROOT/${STAGE_FILES[$1]:-}"; }
|
||||||
key_ok() { # $1=key; exit 2 wenn unbekannt
|
key_ok() {
|
||||||
[ -n "${TARGET_PATHS[$1]:-}" ] && [ -n "${STAGE_FILES[$1]:-}" ] || die "unbekannter KEY '$1' (code-list)"
|
[ -n "${TARGET_PATHS[$1]:-}" ] && [ -n "${STAGE_FILES[$1]:-}" ] || die "unbekannter KEY '$1' (code-list)"
|
||||||
}
|
}
|
||||||
expect_zero() { [ $# -eq 0 ] || die "unerlaubtes Argument '$*'"; }
|
expect_zero() { [ $# -eq 0 ] || die "unerlaubtes Argument '$*'"; }
|
||||||
|
|
||||||
# stage-check: FAIL-CLOSED-Validierung eines Staging-Kandidaten
|
validate_stage() {
|
||||||
validate_stage() { # $1=key
|
|
||||||
local key="$1" file real owner dmode
|
local key="$1" file real owner dmode
|
||||||
file="$(stage_file "$key")"
|
file="$(stage_file "$key")"
|
||||||
[ -e "$file" ] || die "Staging-Kandidat fehlt: $file (lege zuerst die Datei ab)"
|
[ -e "$file" ] || die "Staging-Kandidat fehlt: $file (lege zuerst die Datei ab)"
|
||||||
|
|
@ -97,12 +78,17 @@ validate_stage() { # $1=key
|
||||||
echo "OK: $key -> $file (regular, no symlink, owner rq-host, in staging)"
|
echo "OK: $key -> $file (regular, no symlink, owner rq-host, in staging)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------- dispatch
|
# ---- redact: maskiert Passwort-/Secret-Werte (fuer build-def Anzeige) ----
|
||||||
|
redact_line() {
|
||||||
|
# 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'
|
||||||
|
}
|
||||||
|
|
||||||
SUBCMD="${1:-}"
|
SUBCMD="${1:-}"
|
||||||
shift || true
|
shift || true
|
||||||
|
|
||||||
case "$SUBCMD" in
|
case "$SUBCMD" in
|
||||||
# ---------------- bestehend (read-only / run) ----------------
|
# ---------------- bestehend ----------------
|
||||||
ps) expect_zero "$@"; exec docker ps ;;
|
ps) expect_zero "$@"; exec docker ps ;;
|
||||||
inspect) req_ok; expect_zero "$@"; exec docker inspect "$CONTAINER" ;;
|
inspect) req_ok; expect_zero "$@"; exec docker inspect "$CONTAINER" ;;
|
||||||
logs) req_ok
|
logs) req_ok
|
||||||
|
|
@ -128,7 +114,6 @@ case "$SUBCMD" in
|
||||||
echo "[rq-historical] Start: docker exec $CONTAINER python $SCRIPT ${RUN[*]}" >&2
|
echo "[rq-historical] Start: docker exec $CONTAINER python $SCRIPT ${RUN[*]}" >&2
|
||||||
exec docker exec "$CONTAINER" python "$SCRIPT" "${RUN[@]}"
|
exec docker exec "$CONTAINER" python "$SCRIPT" "${RUN[@]}"
|
||||||
;;
|
;;
|
||||||
# ------------- read-only Code -------------
|
|
||||||
code) req_ok
|
code) req_ok
|
||||||
[ $# -eq 1 ] || die "code braucht genau EINEN Key (code-list)"
|
[ $# -eq 1 ] || die "code braucht genau EINEN Key (code-list)"
|
||||||
p="$(key_path "$1")"; [ -n "$p" ] || die "unbekannter Key 'code $1'"
|
p="$(key_path "$1")"; [ -n "$p" ] || die "unbekannter Key 'code $1'"
|
||||||
|
|
@ -142,15 +127,13 @@ case "$SUBCMD" in
|
||||||
echo "Verfuegbare READ-ONLY-Code-Keys (historical-service):"
|
echo "Verfuegbare READ-ONLY-Code-Keys (historical-service):"
|
||||||
for k in "${!TARGET_PATHS[@]}"; do printf ' %-11s | staging: %-22s -> %s\n' "$k" "${STAGE_FILES[$k]}" "${TARGET_PATHS[$k]}"; done
|
for k in "${!TARGET_PATHS[@]}"; do printf ' %-11s | staging: %-22s -> %s\n' "$k" "${STAGE_FILES[$k]}" "${TARGET_PATHS[$k]}"; done
|
||||||
;;
|
;;
|
||||||
# ------------- dev/test/deploy-kanal -------------
|
|
||||||
stage-dir) expect_zero "$@"
|
stage-dir) expect_zero "$@"
|
||||||
echo "Staging-Root: $STAGING_ROOT (Owner rq-host, keine globalen Schreibrechte)"
|
echo "Staging-Root: $STAGING_ROOT (Owner rq-host, keine globalen Schreibrechte)"
|
||||||
for k in runner backfill pipeline repository provenance trust sanity trust_gate; do
|
for k in runner backfill pipeline repository provenance trust sanity trust_gate; do
|
||||||
printf ' %-11s %s\n' "$k" "$(stage_file "$k")"
|
printf ' %-11s %s\n' "$k" "$(stage_file "$k")"
|
||||||
done
|
done
|
||||||
;;
|
;;
|
||||||
stage-check) [ $# -eq 1 ] || die "stage-check braucht genau EINEN Key"; key_ok "$1"
|
stage-check) [ $# -eq 1 ] || die "stage-check braucht genau EINEN Key"; key_ok "$1"; validate_stage "$1"
|
||||||
validate_stage "$1"
|
|
||||||
;;
|
;;
|
||||||
test) req_ok; [ $# -eq 1 ] || die "test braucht genau EINEN Key"; key_ok "$1"
|
test) req_ok; [ $# -eq 1 ] || die "test braucht genau EINEN Key"; key_ok "$1"
|
||||||
validate_stage "$1"
|
validate_stage "$1"
|
||||||
|
|
@ -161,8 +144,7 @@ case "$SUBCMD" in
|
||||||
echo "PASS: py_compile OK ($file) [isoliert in Container-/tmp, Produktivcode unberuehrt]"
|
echo "PASS: py_compile OK ($file) [isoliert in Container-/tmp, Produktivcode unberuehrt]"
|
||||||
else
|
else
|
||||||
echo "FAIL: py_compile fehlgeschlagen ($file)"
|
echo "FAIL: py_compile fehlgeschlagen ($file)"
|
||||||
docker exec "$CONTAINER" rm -f "$tmp"
|
docker exec "$CONTAINER" rm -f "$tmp"; exit 2
|
||||||
exit 2
|
|
||||||
fi
|
fi
|
||||||
docker exec "$CONTAINER" rm -f "$tmp"
|
docker exec "$CONTAINER" rm -f "$tmp"
|
||||||
;;
|
;;
|
||||||
|
|
@ -178,53 +160,38 @@ case "$SUBCMD" in
|
||||||
echo "HEALTHCHECK OK"
|
echo "HEALTHCHECK OK"
|
||||||
;;
|
;;
|
||||||
backup) req_ok; [ $# -eq 1 ] || die "backup braucht genau EINEN Key"; key_ok "$1"
|
backup) req_ok; [ $# -eq 1 ] || die "backup braucht genau EINEN Key"; key_ok "$1"
|
||||||
p="$(key_path "$1")"
|
p="$(key_path "$1")"; ts="$(date +%Y%m%d_%H%M%S)"; bdir="$BACKUP_ROOT/$1/$ts"
|
||||||
ts="$(date +%Y%m%d_%H%M%S)"
|
|
||||||
bdir="$BACKUP_ROOT/$1/$ts"
|
|
||||||
mkdir -p "$bdir"
|
mkdir -p "$bdir"
|
||||||
docker exec "$CONTAINER" cat "$p" > "$bdir/$(basename "$p")"
|
docker exec "$CONTAINER" cat "$p" > "$bdir/$(basename "$p")"
|
||||||
sha256sum "$bdir/$(basename "$p")" > "$bdir/SHA256"
|
sha256sum "$bdir/$(basename "$p")" > "$bdir/SHA256"
|
||||||
echo "BACKUP: $1 -> $bdir"
|
echo "BACKUP: $1 -> $bdir"; cat "$bdir/SHA256"
|
||||||
cat "$bdir/SHA256"
|
|
||||||
;;
|
;;
|
||||||
deploy) req_ok; [ $# -eq 1 ] || die "deploy braucht genau EINEN Key"; key_ok "$1"
|
deploy) req_ok; [ $# -eq 1 ] || die "deploy braucht genau EINEN Key"; key_ok "$1"
|
||||||
key="$1"; p="$(key_path "$key")"; file="$(stage_file "$key")"
|
key="$1"; p="$(key_path "$key")"; file="$(stage_file "$key")"
|
||||||
# 1) validate (FAIL CLOSED)
|
|
||||||
validate_stage "$key"
|
validate_stage "$key"
|
||||||
# 2) backup erzwingen (muss gelingen)
|
|
||||||
ts="$(date +%Y%m%d_%H%M%S)"; bdir="$BACKUP_ROOT/$key/$ts"
|
ts="$(date +%Y%m%d_%H%M%S)"; bdir="$BACKUP_ROOT/$key/$ts"
|
||||||
mkdir -p "$bdir"
|
mkdir -p "$bdir"
|
||||||
docker exec "$CONTAINER" cat "$p" > "$bdir/$(basename "$p")" || die "Backup fehlgeschlagen -> deploy abgebrochen"
|
docker exec "$CONTAINER" cat "$p" > "$bdir/$(basename "$p")" || die "Backup fehlgeschlagen -> deploy abgebrochen"
|
||||||
sha256sum "$bdir/$(basename "$p")" > "$bdir/SHA256"
|
sha256sum "$bdir/$(basename "$p")" > "$bdir/SHA256"
|
||||||
echo "BACKUP: $bdir"
|
echo "BACKUP: $bdir"
|
||||||
# 3) test (py_compile isoliert)
|
|
||||||
tmp="/tmp/rqdeploy_${key}_$(date +%s).py"
|
tmp="/tmp/rqdeploy_${key}_$(date +%s).py"
|
||||||
docker cp "$file" "$CONTAINER:$tmp"
|
docker cp "$file" "$CONTAINER:$tmp"
|
||||||
if ! docker exec "$CONTAINER" python -m py_compile "$tmp" >/dev/null 2>&1; then
|
if ! docker exec "$CONTAINER" python -m py_compile "$tmp" >/dev/null 2>&1; then
|
||||||
docker exec "$CONTAINER" rm -f "$tmp"
|
docker exec "$CONTAINER" rm -f "$tmp"; die "py_compile FAIL -> deploy abgebrochen (nichts deployt)"
|
||||||
die "py_compile FAIL -> deploy abgebrochen (nichts deployt, kein Rollback noetig)"
|
|
||||||
fi
|
fi
|
||||||
docker exec "$CONTAINER" rm -f "$tmp"
|
docker exec "$CONTAINER" rm -f "$tmp"
|
||||||
# 4) atomic deploy
|
|
||||||
docker cp "$file" "$CONTAINER:$p"
|
docker cp "$file" "$CONTAINER:$p"
|
||||||
# 5) hash-reverify
|
|
||||||
sha_stage="$(sha256sum "$file" | awk '{print $1}')"
|
sha_stage="$(sha256sum "$file" | awk '{print $1}')"
|
||||||
sha_deployed="$(docker exec "$CONTAINER" sha256sum "$p" | awk '{print $1}')"
|
sha_deployed="$(docker exec "$CONTAINER" sha256sum "$p" | awk '{print $1}')"
|
||||||
echo "SHA stage : $sha_stage"
|
echo "SHA stage : $sha_stage"; echo "SHA deployed: $sha_deployed"
|
||||||
echo "SHA deployed: $sha_deployed"
|
|
||||||
if [ "$sha_stage" != "$sha_deployed" ]; then
|
if [ "$sha_stage" != "$sha_deployed" ]; then
|
||||||
echo "HASH-MISMATCH -> rollback"
|
echo "HASH-MISMATCH -> rollback"; docker cp "$bdir/$(basename "$p")" "$CONTAINER:$p"; die "Deploy Hash-Mismatch, zurueckgerollt"
|
||||||
docker cp "$bdir/$(basename "$p")" "$CONTAINER:$p"
|
|
||||||
die "Deploy Hash-Mismatch, zurueckgerollt"
|
|
||||||
fi
|
fi
|
||||||
# 6) healthcheck (nur health/ready, KEIN run)
|
|
||||||
rh="$(docker exec "$CONTAINER" python -c 'import urllib.request; print(urllib.request.urlopen("http://localhost:8000/health", timeout=5).status)' 2>/dev/null || echo N/A)"
|
rh="$(docker exec "$CONTAINER" python -c 'import urllib.request; print(urllib.request.urlopen("http://localhost:8000/health", timeout=5).status)' 2>/dev/null || echo N/A)"
|
||||||
rr="$(docker exec "$CONTAINER" python -c 'import urllib.request; print(urllib.request.urlopen("http://localhost:8000/health/ready", timeout=5).status)' 2>/dev/null || echo N/A)"
|
rr="$(docker exec "$CONTAINER" python -c 'import urllib.request; print(urllib.request.urlopen("http://localhost:8000/health/ready", timeout=5).status)' 2>/dev/null || echo N/A)"
|
||||||
echo "Health: /health=$rh /health/ready=$rr"
|
echo "Health: /health=$rh /health/ready=$rr"
|
||||||
if [ "$rh" != "200" ] || [ "$rr" != "200" ]; then
|
if [ "$rh" != "200" ] || [ "$rr" != "200" ]; then
|
||||||
echo "DEPLOY-HEALTHCHECK FAILED -> rollback"
|
echo "DEPLOY-HEALTHCHECK FAILED -> rollback"; docker cp "$bdir/$(basename "$p")" "$CONTAINER:$p"; die "Rollback ausgefuehrt"
|
||||||
docker cp "$bdir/$(basename "$p")" "$CONTAINER:$p"
|
|
||||||
die "Rollback ausgefuehrt"
|
|
||||||
fi
|
fi
|
||||||
echo "DEPLOY OK: $key -> $p (Backup: $bdir). KEIN backfill/run gestartet."
|
echo "DEPLOY OK: $key -> $p (Backup: $bdir). KEIN backfill/run gestartet."
|
||||||
;;
|
;;
|
||||||
|
|
@ -232,8 +199,7 @@ case "$SUBCMD" in
|
||||||
p="$(key_path "$1")"
|
p="$(key_path "$1")"
|
||||||
bdir="$(ls -1t "$BACKUP_ROOT/$1" 2>/dev/null | head -1)"
|
bdir="$(ls -1t "$BACKUP_ROOT/$1" 2>/dev/null | head -1)"
|
||||||
[ -n "$bdir" ] || die "kein Backup fuer '$1' vorhanden"
|
[ -n "$bdir" ] || die "kein Backup fuer '$1' vorhanden"
|
||||||
bdir="$BACKUP_ROOT/$1/$bdir"
|
bdir="$BACKUP_ROOT/$1/$bdir"; src="$bdir/$(basename "$p")"
|
||||||
src="$bdir/$(basename "$p")"
|
|
||||||
[ -f "$src" ] || die "Backup-Datei fehlt: $src"
|
[ -f "$src" ] || die "Backup-Datei fehlt: $src"
|
||||||
sha_bak="$(sha256sum "$src" | awk '{print $1}')"
|
sha_bak="$(sha256sum "$src" | awk '{print $1}')"
|
||||||
sha_cur="$(docker exec "$CONTAINER" sha256sum "$p" | awk '{print $1}')"
|
sha_cur="$(docker exec "$CONTAINER" sha256sum "$p" | awk '{print $1}')"
|
||||||
|
|
@ -244,6 +210,115 @@ case "$SUBCMD" in
|
||||||
[ "$sha_cur" = "$sha_bak" ] || die "ROLLBACK-HASH-MISMATCH"
|
[ "$sha_cur" = "$sha_bak" ] || die "ROLLBACK-HASH-MISMATCH"
|
||||||
echo "ROLLBACK OK: $key -> $p auf $bdir"
|
echo "ROLLBACK OK: $key -> $p auf $bdir"
|
||||||
;;
|
;;
|
||||||
*) die "Aufruf: ps|inspect|logs|run|code <KEY>|hashes|code-list|stage-dir|stage-check <KEY>|test <KEY>|search-writes|healthcheck|backup <KEY>|deploy <KEY>|rollback <KEY>"
|
|
||||||
|
# ============ SOT-DISCOVERY (read-only, NEU) ============
|
||||||
|
build-def) expect_zero "$@"
|
||||||
|
echo "=== Dockerfile ($HOST_DIR/Dockerfile) — redacted ==="
|
||||||
|
[ -f "$HOST_DIR/Dockerfile" ] || die "Dockerfile nicht gefunden"
|
||||||
|
while IFS= read -r line; do redact_line "$line"; done < "$HOST_DIR/Dockerfile"
|
||||||
|
echo ""
|
||||||
|
echo "=== docker-compose.yml — strukturielle Abschnitte, redacted ==="
|
||||||
|
cf=""
|
||||||
|
if [ -f "$HOST_DIR/docker-compose.yml" ]; then cf="$HOST_DIR/docker-compose.yml";
|
||||||
|
elif [ -f "$HOST_DIR/compose.yml" ]; then cf="$HOST_DIR/compose.yml";
|
||||||
|
elif [ -f "$HOST_DIR/docker-compose.yaml" ]; then cf="$HOST_DIR/docker-compose.yaml"; fi
|
||||||
|
[ -n "$cf" ] || die "keine compose-Datei gefunden"
|
||||||
|
echo "(Quelle: $cf)"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# nur relevante strukturelle Schluessel anzeigen; Environment/Secrets nicht roh
|
||||||
|
case "$line" in
|
||||||
|
*POSTGRES_PASSWORD*|*PG_PASSWORD*|*PASSWORD*|*SECRET*|*TOKEN*) echo " **** REDACTED (Secret-Zeile) ****" ;;
|
||||||
|
*) echo " $line" ;;
|
||||||
|
esac
|
||||||
|
done < "$cf"
|
||||||
|
echo ""
|
||||||
|
echo "=== .dockerignore (falls vorhanden) ==="
|
||||||
|
[ -f "$HOST_DIR/.dockerignore" ] && cat "$HOST_DIR/.dockerignore" || echo "(kein .dockerignore)"
|
||||||
|
echo ""
|
||||||
|
echo "=== Service/Container-Fixierung ==="
|
||||||
|
echo " compose.project=$(docker inspect "$CONTAINER" --format '{{index .Config.Labels "com.docker.compose.project"}}' 2>/dev/null || echo N/A)"
|
||||||
|
echo " compose.service=$(docker inspect "$CONTAINER" --format '{{index .Config.Labels "com.docker.compose.service"}}' 2>/dev/null || echo N/A)"
|
||||||
|
echo " container=$CONTAINER"
|
||||||
|
;;
|
||||||
|
build-files) expect_zero "$@"
|
||||||
|
echo "=== Build-Dateien unter $HOST_DIR (nicht-verzeichnis, nicht .env/.dump/.sql) ==="
|
||||||
|
for f in Dockerfile docker-compose.yml compose.yml docker-compose.yaml .dockerignore requirements.txt; do
|
||||||
|
if [ -e "$HOST_DIR/$f" ]; then printf ' %-22s %s\n' "$f" "EXISTS ($(stat -c '%s' "$HOST_DIR/$f") bytes)"; else printf ' %-22s %s\n' "$f" "(fehlt)"; fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
host-manifest) expect_zero "$@"
|
||||||
|
echo "=== HOST Manifest /opt/historical-v2/app (*.py, keine Symlinks folgen) ==="
|
||||||
|
[ -d "$HOST_APP" ] || die "Host-App-Verzeichnis fehlt: $HOST_APP"
|
||||||
|
# finde reguläre *.py UND Symlinks auf *.py separat
|
||||||
|
(
|
||||||
|
find "$HOST_APP" -type f -name "*.py" -printf '%P %s\n' 2>/dev/null | sort \
|
||||||
|
| while read -r rel size; do
|
||||||
|
sha=$(sha256sum "$HOST_APP/$rel" 2>/dev/null | awk '{print $1}')
|
||||||
|
printf 'FILE\t%s\t%s\t%s\n' "$rel" "$size" "${sha:-ERR}"
|
||||||
|
done
|
||||||
|
find "$HOST_APP" -type l -name "*.py" -printf '%P\n' 2>/dev/null | sort \
|
||||||
|
| while read -r rel; do printf 'SYMLINK\t%s\t0\t<not followed>\n' "$rel"; done
|
||||||
|
)
|
||||||
|
;;
|
||||||
|
host-code) [ $# -eq 1 ] || die "host-code braucht genau EINEN relativen Pfad (unter app/)"
|
||||||
|
rel="$1"
|
||||||
|
[[ "$rel" == *".."* ]] && die "Traversal nicht erlaubt"
|
||||||
|
[[ "$rel" == /* ]] && die "relativer Pfad erwartet, nicht absolut"
|
||||||
|
# realpath zwingend im app-Root
|
||||||
|
real="$(realpath -m "$HOST_APP/$rel")"
|
||||||
|
[[ "$real" == "$HOST_APP/"* ]] || die "Pfad ausserhalb app-Root blockiert"
|
||||||
|
[ -f "$real" ] || die "Datei nicht gefunden oder kein File: $rel"
|
||||||
|
[ -L "$real" ] && die "Symlink-Read nicht erlaubt"
|
||||||
|
# nur .py ausgeben
|
||||||
|
[[ "$rel" == *.py ]] || die "Nur .py-Quelldateien lesbar"
|
||||||
|
echo "--- $rel ($(stat -c '%s' "$real") bytes) ---"
|
||||||
|
exec cat "$real"
|
||||||
|
;;
|
||||||
|
container-manifest) req_ok; expect_zero "$@"
|
||||||
|
echo "=== CONTAINER Manifest historical-service:/app/app (*.py) ==="
|
||||||
|
docker exec "$CONTAINER" sh -c 'cd /app/app && find . -name "*.py" -type f 2>/dev/null | sed "s#^\./##" | sort | while read f; do size=$(stat -c %s "$f"); sha=$(sha256sum "$f" | cut -d" " -f1); printf "FILE\t%s\t%s\t%s\n" "$f" "$size" "$sha"; done'
|
||||||
|
docker exec "$CONTAINER" sh -c 'cd /app/app && find . -name "*.py" -type l 2>/dev/null | sed "s#^\./##" | sort | while read f; do printf "SYMLINK\t%s\t0\t<not followed>\n" "$f"; done' || true
|
||||||
|
;;
|
||||||
|
container-code) req_ok; [ $# -eq 1 ] || die "container-code braucht genau EINEN relativen Pfad (unter /app/app/)"
|
||||||
|
rel="$1"
|
||||||
|
[[ "$rel" == *".."* ]] && die "Traversal nicht erlaubt"
|
||||||
|
[[ "$rel" == /* ]] && die "relativer Pfad erwartet, nicht absolut"
|
||||||
|
target="/app/app/$rel"
|
||||||
|
# sicherstellen Ziel existiert als reguläre Datei & nicht symlink im Container
|
||||||
|
if ! docker exec "$CONTAINER" sh -c "[ -f '$target' ]" 2>/dev/null; then die "Datei nicht gefunden oder kein File im Container: $rel"; fi
|
||||||
|
if docker exec "$CONTAINER" sh -c "[ -L '$target' ]" 2>/dev/null; then die "Symlink-Read im Container nicht erlaubt"; fi
|
||||||
|
[[ "$rel" == *.py ]] || die "Nur .py-Quelldateien lesbar"
|
||||||
|
echo "--- $target ---"
|
||||||
|
exec docker exec "$CONTAINER" cat "$target"
|
||||||
|
;;
|
||||||
|
tree-diff) req_ok; expect_zero "$@"
|
||||||
|
echo "=== TREE-DIFF Host(/opt/historical-v2/app) <-> Container(/app/app) ==="
|
||||||
|
hmap="$(mktemp)"; cmap="$(mktemp)"
|
||||||
|
# Host: 'rel<TAB>sha'
|
||||||
|
find "$HOST_APP" -type f -name "*.py" -printf '%P\n' 2>/dev/null | sort | while IFS= read -r rel; do
|
||||||
|
sha="$(sha256sum "$HOST_APP/$rel" 2>/dev/null | awk '{print $1}')"
|
||||||
|
printf '%s\t%s\n' "$rel" "$sha"
|
||||||
|
done > "$hmap"
|
||||||
|
# Container: 'rel<TAB>sha'
|
||||||
|
docker exec "$CONTAINER" sh -c 'cd /app/app && find . -type f -name "*.py" -printf "%P\n" 2>/dev/null | sort | while IFS= read -r rel; do sha256sum "$rel" 2>/dev/null | awk -v r="$rel" '\''{print r"\t"$1}'\''; done' > "$cmap"
|
||||||
|
tmpunion="$(mktemp)"
|
||||||
|
{ cut -f1 "$hmap"; cut -f1 "$cmap"; } | sort -u > "$tmpunion"
|
||||||
|
echo "HOST_APP=$HOST_APP CONTAINER_APP=/app/app"
|
||||||
|
while IFS= read -r rel; do
|
||||||
|
hs="$(awk -F'\t' -v r="$rel" '$1==r{print $2}' "$hmap")"
|
||||||
|
cs="$(awk -F'\t' -v r="$rel" '$1==r{print $2}' "$cmap")"
|
||||||
|
if [ -n "$hs" ] && [ -n "$cs" ]; then
|
||||||
|
if [ "$hs" = "$cs" ]; then status="MATCH"; else status="DIVERGED"; fi
|
||||||
|
printf '%-18s %s [h:%s c:%s]\n' "$status" "$rel" "${hs:0:12}" "${cs:0:12}"
|
||||||
|
elif [ -n "$hs" ]; then
|
||||||
|
printf '%-18s %s\n' "HOST_ONLY" "$rel"
|
||||||
|
else
|
||||||
|
printf '%-18s %s\n' "MISSING_ON_HOST" "$rel"
|
||||||
|
fi
|
||||||
|
done < "$tmpunion"
|
||||||
|
rm -f "$hmap" "$cmap" "$tmpunion"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*) 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"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue