163 lines
7.2 KiB
Plaintext
163 lines
7.2 KiB
Plaintext
|
|
#!/usr/bin/env bats
|
||
|
|
# tests/lifecycle/bats/all-apps-lifecycle.bats
|
||
|
|
#
|
||
|
|
# DESTRUCTIVE per-app lifecycle matrix across EVERY installed app (breadth) —
|
||
|
|
# the active counterpart to the read-only all-apps-matrix.bats and the ~8 deep
|
||
|
|
# per-app suites. For each installed, NON-protected app it drives:
|
||
|
|
# stop → verify stopped → start → verify running → restart → verify running
|
||
|
|
# and, when ARCHY_ALLOW_CASCADE_DESTRUCTIVE=1, a FULL TEARDOWN:
|
||
|
|
# uninstall (full, removes data) → verify GONE from My Apps (no #13 ghost) →
|
||
|
|
# reinstall from the node catalog → verify running.
|
||
|
|
#
|
||
|
|
# Reinstall spec source: the node catalog (default /opt/archipelago/web-ui/
|
||
|
|
# catalog.json), whose `.apps[]` entries carry {dockerImage, containerConfig} —
|
||
|
|
# exactly what package.install needs. Multi-container stacks (immich, mempool,
|
||
|
|
# netbird, btcpay, indeedhub) ignore dockerImage internally but still require it,
|
||
|
|
# and route to their orchestrator/stack handler; the catalog entry is enough to
|
||
|
|
# trigger the reinstall. An app with no catalog entry is skipped (logged), not
|
||
|
|
# failed — there's no spec to reinstall it from.
|
||
|
|
#
|
||
|
|
# ── PROTECTED apps (NEVER touched — neither cycled nor torn down) ────────────
|
||
|
|
# - chain state, expensive to resync: bitcoin*, electrumx/electrs
|
||
|
|
# - WALLET / financial state, teardown = IRREVERSIBLE fund/credential loss:
|
||
|
|
# lnd, btcpay*, fedimint*
|
||
|
|
# The user asked to protect only bitcoin + electrum; the wallet-bearing apps
|
||
|
|
# are protected by DEFAULT here for safety (a full uninstall destroys their
|
||
|
|
# seed/channel/guardian state). Override the entire set with
|
||
|
|
# ARCHY_MATRIX_PROTECT="space separated ids" to tear them down too — you WILL
|
||
|
|
# lose their data.
|
||
|
|
#
|
||
|
|
# ── Gating ──────────────────────────────────────────────────────────────────
|
||
|
|
# lifecycle tier → ARCHY_ALLOW_DESTRUCTIVE=1
|
||
|
|
# teardown tier → ARCHY_ALLOW_CASCADE_DESTRUCTIVE=1
|
||
|
|
# Both skip otherwise, so this file is inert in a normal run. ON-NODE ONLY
|
||
|
|
# (reads catalog.json on disk + drives the local package lifecycle).
|
||
|
|
#
|
||
|
|
# This is a HEAVY suite: a full teardown of ~15-20 apps re-pulls images and can
|
||
|
|
# run for a long time. Intended as an explicit, supervised coverage pass, not a
|
||
|
|
# per-iteration gate step.
|
||
|
|
|
||
|
|
load '../lib/rpc.bash'
|
||
|
|
|
||
|
|
CATALOG="${ARCHY_CATALOG:-/opt/archipelago/web-ui/catalog.json}"
|
||
|
|
|
||
|
|
# Protected — see header. Override with ARCHY_MATRIX_PROTECT to change the set.
|
||
|
|
PROTECT="${ARCHY_MATRIX_PROTECT:-bitcoin-knots bitcoin-core bitcoin electrumx electrs mempool-electrs lnd btcpay-server btcpayserver btcpay fedimint fedimint-clientd fedimint-gateway}"
|
||
|
|
|
||
|
|
setup_file() {
|
||
|
|
: "${ARCHY_PASSWORD:?Set ARCHY_PASSWORD env var to the UI password}"
|
||
|
|
export ARCHY_FORCE_LOGIN=1
|
||
|
|
rpc_login
|
||
|
|
unset ARCHY_FORCE_LOGIN
|
||
|
|
}
|
||
|
|
|
||
|
|
teardown_file() {
|
||
|
|
rpc_logout_local
|
||
|
|
}
|
||
|
|
|
||
|
|
is_protected() {
|
||
|
|
local id="$1" p
|
||
|
|
for p in $PROTECT; do [[ "$p" == "$id" ]] && return 0; done
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
|
||
|
|
get_package_data() {
|
||
|
|
rpc_result server.get-state '{}' 2>/dev/null | jq -c '.data["package-data"] // {}'
|
||
|
|
}
|
||
|
|
|
||
|
|
# Canonical app ids the catalog can (re)install.
|
||
|
|
catalog_ids() {
|
||
|
|
jq -r '(.apps // [])[].id' "$CATALOG" 2>/dev/null
|
||
|
|
}
|
||
|
|
|
||
|
|
# Installed primary apps we will exercise: catalog ids present in My Apps,
|
||
|
|
# minus the protected set. (Catalog-scoped so we skip sub-containers like
|
||
|
|
# immich_postgres that surface as their own package-data entries.)
|
||
|
|
target_apps() {
|
||
|
|
local pd; pd=$(get_package_data)
|
||
|
|
local id
|
||
|
|
for id in $(catalog_ids); do
|
||
|
|
echo "$pd" | jq -e --arg i "$id" 'has($i)' >/dev/null 2>&1 || continue
|
||
|
|
is_protected "$id" && continue
|
||
|
|
echo "$id"
|
||
|
|
done
|
||
|
|
}
|
||
|
|
|
||
|
|
# Top-level state of an app in My Apps, or "absent" when the entry is gone.
|
||
|
|
app_state() {
|
||
|
|
get_package_data | jq -r --arg i "$1" '.[$i].state // "absent"'
|
||
|
|
}
|
||
|
|
|
||
|
|
# Poll My Apps until app $1 reaches state $2 (or "absent"); $3 = timeout secs.
|
||
|
|
wait_state() {
|
||
|
|
local id="$1" target="$2" timeout="${3:-180}"
|
||
|
|
local deadline=$(( $(date +%s) + timeout ))
|
||
|
|
while (( $(date +%s) < deadline )); do
|
||
|
|
[[ "$(app_state "$id")" == "$target" ]] && return 0
|
||
|
|
sleep 3
|
||
|
|
done
|
||
|
|
echo "wait_state: $id never reached '$target' (last='$(app_state "$id")') within ${timeout}s" >&2
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
|
||
|
|
# Build a package.install payload for $1 from the catalog, or fail (no spec).
|
||
|
|
catalog_install_payload() {
|
||
|
|
local id="$1" img cfg
|
||
|
|
img=$(jq -r --arg i "$id" '(.apps // [])[] | select(.id==$i) | .dockerImage // empty' "$CATALOG")
|
||
|
|
[[ -n "$img" ]] || return 1
|
||
|
|
cfg=$(jq -c --arg i "$id" '(.apps // [])[] | select(.id==$i) | .containerConfig // null' "$CATALOG")
|
||
|
|
if [[ "$cfg" == "null" ]]; then
|
||
|
|
jq -nc --arg id "$id" --arg img "$img" '{id:$id, dockerImage:$img}'
|
||
|
|
else
|
||
|
|
jq -nc --arg id "$id" --arg img "$img" --argjson cfg "$cfg" '{id:$id, dockerImage:$img, containerConfig:$cfg}'
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
# ────────────────────────────────────────────────────────────────────
|
||
|
|
@test "prerequisites: catalog present and at least one target app" {
|
||
|
|
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
|
||
|
|
[[ -f "$CATALOG" ]] || { echo "# catalog not found: $CATALOG" >&3; false; }
|
||
|
|
run target_apps
|
||
|
|
[ "$status" -eq 0 ]
|
||
|
|
[ -n "$output" ] || { echo "# no non-protected installed apps to exercise" >&3; false; }
|
||
|
|
echo "# protected (skipped): $PROTECT" >&3
|
||
|
|
echo "# targets ($(echo "$output" | wc -w)): $(echo $output)" >&3
|
||
|
|
}
|
||
|
|
|
||
|
|
@test "lifecycle: stop → start → restart every non-protected app" {
|
||
|
|
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
|
||
|
|
local fails="" id
|
||
|
|
for id in $(target_apps); do
|
||
|
|
[[ "$(app_state "$id")" == "running" ]] || continue # only cycle running apps
|
||
|
|
rpc_result package.stop "{\"id\":\"$id\"}" >/dev/null 2>&1
|
||
|
|
wait_state "$id" stopped 120 || { fails+="$id:stop "; }
|
||
|
|
rpc_result package.start "{\"id\":\"$id\"}" >/dev/null 2>&1
|
||
|
|
wait_state "$id" running 240 || { fails+="$id:start "; continue; }
|
||
|
|
rpc_result package.restart "{\"id\":\"$id\"}" >/dev/null 2>&1
|
||
|
|
wait_state "$id" running 240 || { fails+="$id:restart "; }
|
||
|
|
done
|
||
|
|
[[ -z "$fails" ]] || { echo "# lifecycle failures: $fails" >&3; false; }
|
||
|
|
}
|
||
|
|
|
||
|
|
@test "teardown: full uninstall (no ghost) → reinstall every non-protected app" {
|
||
|
|
[[ "${ARCHY_ALLOW_CASCADE_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_CASCADE_DESTRUCTIVE not set"
|
||
|
|
local fails="" skipped="" id payload
|
||
|
|
for id in $(target_apps); do
|
||
|
|
if ! payload=$(catalog_install_payload "$id"); then
|
||
|
|
skipped+="$id "
|
||
|
|
continue
|
||
|
|
fi
|
||
|
|
rpc_result package.uninstall "{\"id\":\"$id\"}" >/dev/null 2>&1
|
||
|
|
# No ghost: the entry must leave My Apps (the #13 class). 71cc9ac4 bounds the
|
||
|
|
# teardown so this can no longer hang indefinitely.
|
||
|
|
if ! wait_state "$id" absent 300; then
|
||
|
|
fails+="$id:ghost "
|
||
|
|
continue
|
||
|
|
fi
|
||
|
|
rpc_result package.install "$payload" >/dev/null 2>&1
|
||
|
|
wait_state "$id" running 420 || fails+="$id:reinstall "
|
||
|
|
done
|
||
|
|
[[ -n "$skipped" ]] && echo "# skipped (no catalog spec to reinstall from): $skipped" >&3
|
||
|
|
[[ -z "$fails" ]] || { echo "# teardown failures: $fails" >&3; false; }
|
||
|
|
}
|