#!/usr/bin/env bash set -euo pipefail REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" OUTPUT_PATH="${OUTPUT_PATH:-/mnt/user/services/posture-check/compose-runtime-drift-last.json}" NTFY_SCRIPT="${NTFY_SCRIPT:-$REPO_ROOT/ops/restore-tests/send-ntfy.sh}" NTFY_TOPIC="${NTFY_TOPIC:-kallilab-warning}" SEND_NTFY="${SEND_NTFY:-1}" TMP_DIR="${TMP_DIR:-/tmp/kallilab-compose-runtime-drift}" mkdir -p "$TMP_DIR" RESULTS_FILE="$TMP_DIR/results.$$" : > "$RESULTS_FILE" trap 'rm -f "$RESULTS_FILE"' EXIT json_escape() { sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g' } add_result() { printf '%s\t%s\t%s\n' "$1" "$2" "$3" >> "$RESULTS_FILE" } parse_compose() { local compose="$1" awk ' /^ [A-Za-z0-9_.-]+:/ { service=$1 sub(/:$/, "", service) image="" container=service } service && /^ image:/ { image=$2 gsub(/["'\'']/, "", image) } service && /^ container_name:/ { container=$2 gsub(/["'\'']/, "", container) } service && image && container { print container "\t" image service="" image="" container="" } ' "$compose" } while IFS= read -r -d '' compose; do while IFS="$(printf '\t')" read -r container expected_image; do [ -n "$container" ] || continue if ! runtime_image="$(docker inspect --format '{{.Config.Image}}' "$container" 2>/dev/null)"; then add_result "warning" "$container" "Container missing for compose image $expected_image ($compose)" continue fi if [ "$runtime_image" = "$expected_image" ]; then add_result "ok" "$container" "Runtime image matches $expected_image" else add_result "warning" "$container" "Runtime image '$runtime_image' differs from compose '$expected_image' ($compose)" fi done < <(parse_compose "$compose") done < <(find "$REPO_ROOT" -path "$REPO_ROOT/.git" -prune -o -type f \( -name docker-compose.yml -o -name docker-compose.yaml -o -name compose.yml -o -name compose.yaml \) -print0) timestamp="$(date -Iseconds)" warning_count="$(awk -F '\t' '$1 == "warning" { count++ } END { print count + 0 }' "$RESULTS_FILE")" status="ok" [ "$warning_count" -gt 0 ] && status="warning" mkdir -p "$(dirname "$OUTPUT_PATH")" { printf '{\n' printf ' "timestamp": "%s",\n' "$(printf '%s' "$timestamp" | json_escape)" printf ' "status": "%s",\n' "$status" printf ' "warning_count": %s,\n' "$warning_count" printf ' "checks": [\n' first=1 while IFS="$(printf '\t')" read -r severity name message; do if [ "$first" -eq 0 ]; then printf ',\n'; fi first=0 printf ' {"severity":"%s","name":"%s","message":"%s"}' \ "$(printf '%s' "$severity" | json_escape)" \ "$(printf '%s' "$name" | json_escape)" \ "$(printf '%s' "$message" | json_escape)" done < "$RESULTS_FILE" printf '\n ]\n}\n' } > "$OUTPUT_PATH.tmp" mv "$OUTPUT_PATH.tmp" "$OUTPUT_PATH" cat "$OUTPUT_PATH" if [ "$warning_count" -gt 0 ]; then if [ "$SEND_NTFY" = "1" ] && [ -x "$NTFY_SCRIPT" ]; then "$NTFY_SCRIPT" "$NTFY_TOPIC" "Compose/runtime drift detected" "$warning_count drift warning(s). See $OUTPUT_PATH" high || true fi exit 1 fi