f775685cd2
Add endpoint-agnostic Healthchecks pings to the three remaining scheduled host-audit jobs via an EXIT-trap merge (start + success/fail), so the body of each script (incl. the 1400-line daily-status-report) stays untouched. Exit 0/1/2 = ran (ok/warning/critical); only rc>2 pings /fail. Capability URLs come from per-job host secret files (healthchecks_<job>_url), never in the repo. bash -n verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
3.9 KiB
Bash
Executable File
116 lines
3.9 KiB
Bash
Executable File
#!/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:-homelab-alerts}"
|
|
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"
|
|
# Healthchecks Heartbeat (endpoint-agnostisch; Capability-URL ist ein Secret, nie ins Repo)
|
|
HC_URL_FILE="${HC_URL_FILE:-/mnt/user/appdata/secrets/healthchecks_compose_drift_url}"
|
|
hc_url=""; [ -r "$HC_URL_FILE" ] && hc_url="$(tr -d '[:space:]' < "$HC_URL_FILE")"
|
|
hc_ping() { [ -n "$hc_url" ] || return 0; curl -fsS -m 10 --retry 3 "${hc_url}${1:-}" >/dev/null 2>&1 || true; }
|
|
trap 'hc_rc=$?; rm -f "$RESULTS_FILE"; [ "$hc_rc" -le 2 ] && hc_ping "" || hc_ping "/fail"' EXIT
|
|
hc_ping "/start"
|
|
|
|
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 '
|
|
function clean(value) {
|
|
gsub(/\r/, "", value)
|
|
gsub(/["'\''"]/, "", value)
|
|
return value
|
|
}
|
|
function emit() {
|
|
if (service && image && !has_profile) {
|
|
print clean(container) "\t" clean(image)
|
|
}
|
|
}
|
|
/^ [A-Za-z0-9_.-]+:/ {
|
|
emit()
|
|
service=$1
|
|
sub(/:$/, "", service)
|
|
image=""
|
|
container=service
|
|
has_profile=0
|
|
next
|
|
}
|
|
service && /^ image:/ {
|
|
image=$0
|
|
sub(/^[[:space:]]*image:[[:space:]]*/, "", image)
|
|
next
|
|
}
|
|
service && /^ container_name:/ {
|
|
container=$0
|
|
sub(/^[[:space:]]*container_name:[[:space:]]*/, "", container)
|
|
next
|
|
}
|
|
service && /^ profiles:/ {
|
|
has_profile=1
|
|
next
|
|
}
|
|
END { emit() }
|
|
' "$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
|