Files
archy/scripts/app-surface-smoke-test.sh
2026-04-30 16:37:54 -04:00

162 lines
5.2 KiB
Bash
Executable File

#!/bin/bash
#
# App surface smoke test.
#
# Verifies that installed containers have their published host ports listening
# and that known nginx app proxy paths return a non-5xx response. This catches
# the common "container is running but UI disappeared" failure mode.
#
# Usage:
# scripts/app-surface-smoke-test.sh --target archipelago@192.168.1.228 --ssh-key /path/key
set -euo pipefail
TARGET=""
SSH_KEY="${ARCHIPELAGO_SSH_KEY:-}"
SSH_EXTRA=()
while [ "$#" -gt 0 ]; do
case "$1" in
--target) TARGET="${2:-}"; shift 2 ;;
--ssh-key) SSH_KEY="${2:-}"; shift 2 ;;
--ssh-option) SSH_EXTRA+=("-o" "${2:-}"); shift 2 ;;
-h|--help) sed -n '1,12p' "$0"; exit 0 ;;
*) echo "unknown argument: $1" >&2; exit 2 ;;
esac
done
[ -n "$TARGET" ] || { echo "--target is required" >&2; exit 2; }
SSH_OPTS=(-F /dev/null -o BatchMode=yes -o PreferredAuthentications=publickey -o PasswordAuthentication=no)
[ -n "$SSH_KEY" ] && SSH_OPTS+=(-i "$SSH_KEY")
SSH_OPTS+=("${SSH_EXTRA[@]}")
ssh_run() {
ssh "${SSH_OPTS[@]}" "$TARGET" "$@"
}
ssh_run 'bash -s' <<'REMOTE'
set -u
pass=0
fail=0
ok() { echo " PASS $*"; pass=$((pass + 1)); }
bad() { echo " FAIL $*"; fail=$((fail + 1)); }
container_exists() {
podman ps -a --format '{{.Names}}' 2>/dev/null | grep -qx "$1"
}
port_listening() {
ss -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "(^|:)$1$"
}
http_code() {
local url="$1" code
for _ in 1 2 3; do
code=$(curl -ksS -o /dev/null -w '%{http_code}' --max-time 12 "$url" 2>/dev/null || true)
[ -n "$code" ] || code=000
[ "$code" != "000" ] && { echo "$code"; return; }
sleep 2
done
echo "$code"
}
http_post_code() {
local url="$1" code
for _ in 1 2 3; do
code=$(curl -ksS -o /dev/null -w '%{http_code}' --max-time 25 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"getblockchaininfo","params":[]}' \
"$url" 2>/dev/null || true)
[ -n "$code" ] || code=000
[ "$code" != "000" ] && { echo "$code"; return; }
sleep 2
done
echo "$code"
}
assert_http() {
local label="$1" url="$2" code
code=$(http_code "$url")
case "$code" in
200|204|301|302|307|308|401|403) ok "$label HTTP $code" ;;
*) bad "$label HTTP $code ($url)" ;;
esac
}
assert_http_post() {
local label="$1" url="$2" code
code=$(http_post_code "$url")
case "$code" in
200|204|401|403) ok "$label HTTP POST $code" ;;
*) bad "$label HTTP POST $code ($url)" ;;
esac
}
assert_container_ports() {
local name="$1" ports port missing=0
container_exists "$name" || return 0
ports=$(podman inspect "$name" --format '{{range $p,$bindings := .NetworkSettings.Ports}}{{if $bindings}}{{range $bindings}}{{.HostPort}}{{"\n"}}{{end}}{{end}}{{end}}' 2>/dev/null | sort -u)
[ -n "$ports" ] || return 0
while IFS= read -r port; do
[ -n "$port" ] || continue
if port_listening "$port"; then
ok "$name port $port listening"
else
bad "$name port $port missing listener"
missing=1
fi
done <<< "$ports"
return "$missing"
}
assert_env_contains() {
local name="$1" key="$2" needle="$3" val
container_exists "$name" || return 0
val=$(podman inspect "$name" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | sed -n "s/^${key}=//p" | head -n 1)
if [ -n "$val" ] && printf '%s' "$val" | grep -qF "$needle"; then
ok "$name env $key"
else
bad "$name env $key missing $needle"
fi
}
echo "[surface] host=$(hostname) ip=$(hostname -I 2>/dev/null | awk '{print $1}')"
for c in $(podman ps -a --format '{{.Names}}' 2>/dev/null | sort); do
assert_container_ports "$c" || true
done
container_exists archy-bitcoin-ui && {
assert_http "bitcoin-ui" "http://127.0.0.1/app/bitcoin-ui/"
assert_http "bitcoin status" "http://127.0.0.1/app/bitcoin-ui/bitcoin-status"
assert_http_post "bitcoin rpc proxy" "http://127.0.0.1/app/bitcoin-ui/bitcoin-rpc/"
}
container_exists archy-electrs-ui && {
assert_http "electrumx ui" "http://127.0.0.1/app/electrumx/"
assert_http "electrumx status" "http://127.0.0.1/app/electrumx/electrs-status"
assert_http "electrs legacy status" "http://127.0.0.1/app/electrs/electrs-status"
}
container_exists mempool && assert_http "mempool ui" "http://127.0.0.1/app/mempool/"
container_exists indeedhub && assert_http "indeedhub ui" "http://127.0.0.1:7778/"
container_exists uptime-kuma && assert_http "uptime-kuma" "http://127.0.0.1/app/uptime-kuma/"
container_exists filebrowser && assert_http "filebrowser" "http://127.0.0.1/app/filebrowser/"
container_exists searxng && assert_http "searxng" "http://127.0.0.1/app/searxng/"
container_exists grafana && assert_http "grafana" "http://127.0.0.1/app/grafana/"
container_exists portainer && assert_http "portainer" "http://127.0.0.1/app/portainer/"
container_exists vaultwarden && assert_http "vaultwarden" "http://127.0.0.1/app/vaultwarden/"
container_exists nextcloud && assert_http "nextcloud" "http://127.0.0.1/app/nextcloud/"
container_exists archy-nbxplorer && assert_env_contains "archy-nbxplorer" "NBXPLORER_POSTGRES" "Database=nbxplorer"
container_exists btcpay-server && {
assert_env_contains "btcpay-server" "BTCPAY_POSTGRES" "Database=btcpay"
assert_http "btcpay" "http://127.0.0.1/app/btcpay/"
}
echo "[surface] summary: pass=$pass fail=$fail"
[ "$fail" -eq 0 ]
REMOTE