archy/tests/lifecycle/remote-lifecycle.sh
2026-06-12 03:00:15 -04:00

632 lines
22 KiB
Bash
Executable File

#!/usr/bin/env bash
# Remote app lifecycle runner for Archipelago nodes.
#
# Exercises the same public surface the UI uses:
# - JSON-RPC package.install/start/stop/restart/uninstall
# - HTTPS/direct-port launch probes from appSessionConfig.ts
#
# Default mode is audit-only. Use ARCHY_FULL_LIFECYCLE=1 for destructive
# preserve-data cycles: install -> launch -> stop -> start -> restart ->
# uninstall(preserve_data=true) -> reinstall -> launch.
set -euo pipefail
ARCHY_HOST="${ARCHY_HOST:-}"
ARCHY_SCHEME="${ARCHY_SCHEME:-https}"
ARCHY_PASSWORD="${ARCHY_PASSWORD:-}"
ARCHY_ITERATIONS="${ARCHY_ITERATIONS:-1}"
ARCHY_FULL_LIFECYCLE="${ARCHY_FULL_LIFECYCLE:-0}"
ARCHY_APPS="${ARCHY_APPS:-}"
ARCHY_TIMEOUT="${ARCHY_TIMEOUT:-900}"
ARCHY_STABILITY_SECONDS="${ARCHY_STABILITY_SECONDS:-5}"
ARCHY_ALLOW_BITCOIN_SWAP="${ARCHY_ALLOW_BITCOIN_SWAP:-0}"
ARCHY_APP_CATALOG="${ARCHY_APP_CATALOG:-}"
ARCHY_PRUNED_NODE="${ARCHY_PRUNED_NODE:-auto}"
if [[ -z "$ARCHY_HOST" || -z "$ARCHY_PASSWORD" ]]; then
echo "ARCHY_HOST and ARCHY_PASSWORD are required" >&2
exit 2
fi
if ! [[ "$ARCHY_ITERATIONS" =~ ^[1-9][0-9]*$ ]]; then
echo "ARCHY_ITERATIONS must be a positive integer" >&2
exit 2
fi
if ! [[ "$ARCHY_STABILITY_SECONDS" =~ ^[0-9]+$ ]]; then
echo "ARCHY_STABILITY_SECONDS must be a non-negative integer" >&2
exit 2
fi
BASE_URL="${ARCHY_SCHEME}://${ARCHY_HOST}"
SESSION=""
CSRF=""
CATALOG_FILE=""
ALL_APPS=(
bitcoin-knots
btcpay-server
lnd
mempool
homeassistant
grafana
searxng
ollama
nextcloud
vaultwarden
jellyfin
photoprism
immich
filebrowser
nginx-proxy-manager
portainer
tailscale
uptime-kuma
electrumx
fedimint
indeedhub
dwn
botfights
gitea
)
ARCHIVAL_ONLY_APPS=(
electrumx
mempool
)
app_in_list() {
local needle="$1"
shift
local item
for item in "$@"; do
[[ "$item" == "$needle" ]] && return 0
done
return 1
}
fetch_catalog() {
CATALOG_FILE=$(mktemp)
if [[ -n "$ARCHY_APP_CATALOG" ]]; then
cp "$ARCHY_APP_CATALOG" "$CATALOG_FILE"
return 0
fi
if curl -skfL --connect-timeout 8 -m 30 "${BASE_URL}/api/app-catalog" -o "$CATALOG_FILE" \
&& jq -e '.apps | length > 0' "$CATALOG_FILE" >/dev/null; then
return 0
fi
curl -skfL --connect-timeout 8 -m 30 "${BASE_URL}/catalog.json" -o "$CATALOG_FILE"
jq -e '.apps | length > 0' "$CATALOG_FILE" >/dev/null
}
catalog_app_ids() {
jq -r '.apps[] | select((.dockerImage // "") != "") | .id' "$CATALOG_FILE"
}
catalog_app_json() {
local app="$1"
[[ -n "$CATALOG_FILE" && -r "$CATALOG_FILE" ]] || return 1
jq -c --arg app "$app" '
.registry as $registry
| .apps[]
| select(.id == $app)
| .dockerImage = (if ((.dockerImage // "") | contains("/")) then .dockerImage else ($registry + "/" + .dockerImage) end)
' "$CATALOG_FILE" | head -n 1
}
is_pruned_node() {
case "$ARCHY_PRUNED_NODE" in
1|true|yes) return 0 ;;
0|false|no) return 1 ;;
esac
local pass body
pass=$(ssh "${ARCHY_HOST}" 'sudo cat /var/lib/archipelago/secrets/bitcoin-rpc-password 2>/dev/null || cat /var/lib/archipelago/secrets/bitcoin-rpc-password 2>/dev/null' 2>/dev/null || true)
[[ -n "$pass" ]] || return 1
body=$(curl -fsS --max-time 20 \
--user "archipelago:${pass}" \
--data-binary '{"jsonrpc":"1.0","id":"remote-lifecycle","method":"getblockchaininfo","params":[]}' \
-H 'content-type: text/plain;' \
"http://${ARCHY_HOST}:8332/" 2>/dev/null || true)
printf '%s' "$body" | jq -e '.result.pruned == true' >/dev/null 2>&1
}
image_for() {
case "$1" in
bitcoin-knots) echo "146.59.87.168:3000/lfg2025/bitcoin-knots:latest" ;;
bitcoin-core) echo "docker.io/bitcoin/bitcoin:28.4" ;;
btcpay-server) echo "docker.io/btcpayserver/btcpayserver:2.3.9" ;;
lnd) echo "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta" ;;
mempool) echo "146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.0" ;;
homeassistant) echo "146.59.87.168:3000/lfg2025/home-assistant:2024.1" ;;
grafana) echo "146.59.87.168:3000/lfg2025/grafana:10.2.0" ;;
searxng) echo "146.59.87.168:3000/lfg2025/searxng:latest" ;;
ollama) echo "146.59.87.168:3000/lfg2025/ollama:latest" ;;
nextcloud) echo "146.59.87.168:3000/lfg2025/nextcloud:28" ;;
vaultwarden) echo "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine" ;;
jellyfin) echo "146.59.87.168:3000/lfg2025/jellyfin:10.8.13" ;;
photoprism) echo "146.59.87.168:3000/lfg2025/photoprism:240915" ;;
immich) echo "146.59.87.168:3000/lfg2025/immich-server:release" ;;
filebrowser) echo "146.59.87.168:3000/lfg2025/filebrowser:v2.27.0" ;;
nginx-proxy-manager) echo "146.59.87.168:3000/lfg2025/nginx-proxy-manager:latest" ;;
portainer) echo "146.59.87.168:3000/lfg2025/portainer:latest" ;;
uptime-kuma) echo "146.59.87.168:3000/lfg2025/uptime-kuma:1" ;;
tailscale) echo "146.59.87.168:3000/lfg2025/tailscale:stable" ;;
electrumx) echo "146.59.87.168:3000/lfg2025/electrumx:v1.18.0" ;;
fedimint) echo "146.59.87.168:3000/lfg2025/fedimintd:v0.10.0" ;;
indeedhub) echo "146.59.87.168:3000/lfg2025/indeedhub:1.0.0" ;;
botfights) echo "146.59.87.168:3000/lfg2025/botfights:1.1.0" ;;
gitea) echo "docker.io/gitea/gitea:1.23" ;;
meshtastic) echo "docker.io/meshtastic/meshtasticd:daily-alpine" ;;
*) return 1 ;;
esac
}
launch_url_for() {
case "$1" in
bitcoin-knots|bitcoin-core|bitcoin-ui) echo "http://${ARCHY_HOST}:8334/" ;;
lnd|archy-lnd-ui) echo "http://${ARCHY_HOST}:18083/" ;;
electrumx|electrs|mempool-electrs|archy-electrs-ui) echo "http://${ARCHY_HOST}:50002/" ;;
mempool|mempool-web|archy-mempool-web) echo "http://${ARCHY_HOST}:4080/" ;;
fedimint|fedimintd) echo "http://${ARCHY_HOST}:8175/" ;;
fedimint-gateway) echo "http://${ARCHY_HOST}:8176/" ;;
filebrowser) echo "http://${ARCHY_HOST}:8083/" ;;
grafana) echo "http://${ARCHY_HOST}:3000/" ;;
btcpay-server) echo "http://${ARCHY_HOST}:23000/" ;;
jellyfin) echo "http://${ARCHY_HOST}:8096/" ;;
searxng) echo "http://${ARCHY_HOST}:8888/" ;;
ollama) echo "http://${ARCHY_HOST}:11434/" ;;
immich|immich_server) echo "http://${ARCHY_HOST}:2283/" ;;
portainer) echo "http://${ARCHY_HOST}:9000/" ;;
nginx-proxy-manager) echo "http://${ARCHY_HOST}:8081/" ;;
tailscale) echo "http://${ARCHY_HOST}:8240/" ;;
uptime-kuma) echo "http://${ARCHY_HOST}:3002/" ;;
homeassistant) echo "http://${ARCHY_HOST}:8123/" ;;
vaultwarden) echo "http://${ARCHY_HOST}:8082/" ;;
photoprism) echo "http://${ARCHY_HOST}:2342/" ;;
dwn) echo "http://${ARCHY_HOST}:3100/" ;;
botfights) echo "http://${ARCHY_HOST}:9100/" ;;
gitea) echo "http://${ARCHY_HOST}:3001/" ;;
indeedhub) echo "http://${ARCHY_HOST}:7778/" ;;
*) return 1 ;;
esac
}
rpc_login() {
local headers body err
headers=$(mktemp)
body=$(curl -sk -D "$headers" -X POST "${BASE_URL}/rpc/v1" \
-H 'Content-Type: application/json' \
--data-raw "$(jq -nc --arg p "$ARCHY_PASSWORD" '{jsonrpc:"2.0",method:"auth.login",params:{password:$p},id:1}')")
err=$(printf '%s' "$body" | jq -r '.error.message // empty')
if [[ -n "$err" ]]; then
rm -f "$headers"
echo "login failed on $ARCHY_HOST: $err" >&2
return 1
fi
SESSION=$(grep -i '^set-cookie: session=' "$headers" | head -1 | sed -E 's/.*session=([^;]+).*/\1/' | tr -d '\r')
CSRF=$(grep -i '^set-cookie: csrf_token=' "$headers" | head -1 | sed -E 's/.*csrf_token=([^;]+).*/\1/' | tr -d '\r')
rm -f "$headers"
[[ -n "$SESSION" && -n "$CSRF" ]]
}
rpc_call() {
local method="$1" params="${2:-null}" id="${3:-2}"
local payload
if [[ "$params" == "null" ]]; then
payload=$(jq -nc --arg m "$method" --argjson id "$id" '{jsonrpc:"2.0",method:$m,id:$id}')
else
payload=$(jq -nc --arg m "$method" --argjson p "$params" --argjson id "$id" '{jsonrpc:"2.0",method:$m,params:$p,id:$id}')
fi
curl -sk -X POST "${BASE_URL}/rpc/v1" \
--connect-timeout 8 \
-m "${ARCHY_RPC_TIMEOUT:-60}" \
-H 'Content-Type: application/json' \
-H "Cookie: session=${SESSION}; csrf_token=${CSRF}" \
-H "X-CSRF-Token: ${CSRF}" \
--data-raw "$payload"
}
rpc_result() {
local resp err
resp=$(rpc_call "$@")
err=$(printf '%s' "$resp" | jq -r '.error.message // empty')
if [[ -n "$err" ]]; then
echo "$err" >&2
return 1
fi
printf '%s' "$resp" | jq '.result'
}
container_state() {
local app="$1"
rpc_result container-list | jq -r --arg app "$app" '
(map(select(.name == $app or .id == $app)) | first | .state // "absent") | ascii_downcase
'
}
container_health() {
local app="$1" health
health=$(
ARCHY_RPC_TIMEOUT="${ARCHY_HEALTH_RPC_TIMEOUT:-20}" \
rpc_result container-health "$(jq -nc --arg app "$app" '{app_id:$app}')" \
| jq -r --arg app "$app" '.[$app] // "unknown" | ascii_downcase'
) || health=unknown
if [[ "$app" == "indeedhub" && "$health" != "healthy" ]] && probe_launch "$app" >/dev/null 2>&1; then
health=healthy
fi
printf '%s\n' "$health"
}
assert_container_healthy() {
local app="$1" health
health=$(container_health "$app" 2>/dev/null || echo unknown)
case "$health" in
healthy) return 0 ;;
*) echo "bad health: $app is $health" >&2; return 1 ;;
esac
}
wait_container_healthy() {
local app="$1" timeout="${2:-$ARCHY_TIMEOUT}" deadline health
deadline=$(( $(date +%s) + timeout ))
while (( $(date +%s) < deadline )); do
health=$(container_health "$app" 2>/dev/null || echo unknown)
if [[ "$health" == "healthy" ]]; then return 0; fi
sleep 5
done
echo "bad health: $app is ${health:-unknown}" >&2
return 1
}
observe_stable() {
local app="$1" seconds="${2:-$ARCHY_STABILITY_SECONDS}" deadline state
(( seconds == 0 )) && return 0
deadline=$(( $(date +%s) + seconds ))
while (( $(date +%s) < deadline )); do
state=$(container_state "$app" 2>/dev/null || echo unknown)
if [[ "$state" != "running" ]]; then
if [[ "$app" == "indeedhub" ]] && probe_launch "$app" >/dev/null 2>&1; then
sleep 5
continue
fi
echo "stability failed: $app left running state (last=$state)" >&2
return 1
fi
assert_container_healthy "$app" || return 1
sleep 5
done
}
wait_state() {
local app="$1" target="$2" timeout="${3:-$ARCHY_TIMEOUT}"
local deadline state
deadline=$(( $(date +%s) + timeout ))
while (( $(date +%s) < deadline )); do
state=$(container_state "$app" 2>/dev/null || echo unknown)
if [[ "$target" == "absent" && "$state" == "absent" ]]; then return 0; fi
if [[ "$target" == "stopped" && "$state" == "absent" ]]; then return 0; fi
if [[ "$target" != "absent" && "$state" == "$target" ]]; then return 0; fi
if [[ "$app" == "indeedhub" && "$target" == "running" ]] && probe_launch "$app" >/dev/null 2>&1; then return 0; fi
sleep 5
done
echo "$app did not reach $target within ${timeout}s (last=$state)" >&2
return 1
}
wait_absent_settled() {
local app="$1" timeout="${2:-$ARCHY_TIMEOUT}"
local deadline state seen_absent=0
deadline=$(( $(date +%s) + timeout ))
while (( $(date +%s) < deadline )); do
state=$(container_state "$app" 2>/dev/null || echo unknown)
if [[ "$state" == "absent" ]]; then
if (( seen_absent == 1 )); then return 0; fi
seen_absent=1
else
seen_absent=0
fi
sleep 5
done
echo "$app did not settle absent within ${timeout}s (last=$state)" >&2
return 1
}
wait_not_installing() {
local app="$1" timeout="${2:-$ARCHY_TIMEOUT}"
local deadline state
deadline=$(( $(date +%s) + timeout ))
while (( $(date +%s) < deadline )); do
state=$(container_state "$app" 2>/dev/null || echo unknown)
case "$state" in
installing|starting|restarting|updating) sleep 5 ;;
*) return 0 ;;
esac
done
echo "$app did not settle from install transition within ${timeout}s (last=$state)" >&2
return 1
}
probe_launch() {
local app="$1" url code bytes body
url=$(launch_url_for "$app") || return 0
body=$(mktemp)
code=$(curl -skL --connect-timeout 8 -m 20 -o "$body" -w '%{http_code}' "$url" || true)
bytes=$(wc -c < "$body" 2>/dev/null || printf 0)
if [[ "$code" != "200" || "$bytes" -eq 0 ]]; then
echo "launch failed: $app $url status=$code bytes=$bytes" >&2
rm -f "$body"
return 1
fi
case "$app" in
lnd) probe_lnd_wallet_connect "$body" || { rm -f "$body"; return 1; } ;;
electrumx|electrs|mempool-electrs) probe_electrum_wallet_connect "$body" || { rm -f "$body"; return 1; } ;;
indeedhub) probe_indeedhub_nostr_signer "$body" || { rm -f "$body"; return 1; } ;;
tailscale) probe_tailscale_login_ui "$body" || { rm -f "$body"; return 1; } ;;
esac
rm -f "$body"
}
wait_launch() {
local app="$1" timeout="${2:-$ARCHY_TIMEOUT}" deadline
deadline=$(( $(date +%s) + timeout ))
while (( $(date +%s) < deadline )); do
if probe_launch "$app" >/dev/null 2>&1; then return 0; fi
sleep 5
done
probe_launch "$app"
}
assert_launch_metadata() {
local app="$1" timeout="${2:-$ARCHY_TIMEOUT}" deadline lan
launch_url_for "$app" >/dev/null 2>&1 || return 0
deadline=$(( $(date +%s) + timeout ))
while (( $(date +%s) < deadline )); do
lan=$(rpc_result container-list | jq -r --arg app "$app" '
(map(select(.name == $app or .id == $app)) | first | .lan_address // "")
')
if [[ -n "$lan" && "$lan" != "null" ]]; then return 0; fi
sleep 5
done
if [[ -z "${lan:-}" || "$lan" == "null" ]]; then
echo "launch metadata missing: $app has no lan_address" >&2
return 1
fi
}
require_body() {
local body="$1" needle="$2" label="$3"
if ! grep -Fq "$needle" "$body"; then
echo "launch missing $label: $needle" >&2
return 1
fi
}
probe_lnd_wallet_connect() {
local body="$1" info err
require_body "$body" 'Connect Your Wallet' 'LND wallet heading' || return 1
require_body "$body" 'id="lndQrBox"' 'LND QR container' || return 1
require_body "$body" 'id="connHost"' 'LND host field' || return 1
require_body "$body" 'value="rest-tor"' 'LND REST Tor mode' || return 1
require_body "$body" 'value="grpc-tor"' 'LND gRPC Tor mode' || return 1
require_body "$body" 'value="rest-local"' 'LND REST local mode' || return 1
require_body "$body" 'value="grpc-local"' 'LND gRPC local mode' || return 1
require_body "$body" 'Copy lndconnect URI' 'LND connect URI button' || return 1
info=$(curl -skL --connect-timeout 8 -m 20 \
-H "Cookie: session=${SESSION}; csrf_token=${CSRF}" \
-H "X-CSRF-Token: ${CSRF}" \
"${BASE_URL}/lnd-connect-info" || true)
err=$(printf '%s' "$info" | jq -r '.error // empty' 2>/dev/null || true)
if [[ -n "$err" ]]; then
echo "lnd connect info error: $err" >&2
return 1
fi
printf '%s' "$info" | jq -e '
(.cert_base64url | type == "string" and length > 100) and
(.macaroon_base64url | type == "string" and length > 50) and
(.tor_onion | type == "string" and test("^[a-z2-7]+\\.onion$")) and
(.rest_port == 18080) and
(.grpc_port == 10009)
' >/dev/null || {
echo "lnd connect info incomplete: $info" >&2
return 1
}
}
probe_electrum_wallet_connect() {
local body="$1"
require_body "$body" 'Connect Your Wallet' 'Electrum wallet heading' || return 1
require_body "$body" 'id="qrLocalBox"' 'Electrum local QR container' || return 1
require_body "$body" 'id="qrTorBox"' 'Electrum Tor QR container' || return 1
require_body "$body" 'id="localAddress"' 'Electrum local address field' || return 1
require_body "$body" 'id="torAddress"' 'Electrum Tor address field' || return 1
require_body "$body" '50001' 'Electrum wallet port' || return 1
require_body "$body" 'renderQR' 'Electrum QR renderer' || return 1
curl -skL --connect-timeout 8 -m 20 -f "http://${ARCHY_HOST}:50002/qrcode.js" >/dev/null || {
echo "electrum qrcode.js unavailable" >&2
return 1
}
local status
status=$(curl -skL --connect-timeout 8 -m 20 "${BASE_URL}/electrs-status" || true)
printf '%s' "$status" | jq -e '(.tor_onion | type == "string" and test("^[a-z2-7]+\\.onion$"))' >/dev/null || {
echo "electrum tor connection info incomplete: $status" >&2
return 1
}
}
probe_indeedhub_nostr_signer() {
local body="$1" provider pubkey signed now
require_body "$body" '/nostr-provider.js' 'IndeedHub Nostr provider injection' || return 1
provider=$(curl -skL --connect-timeout 8 -m 20 "http://${ARCHY_HOST}:7778/nostr-provider.js" || true)
if [[ -z "$provider" ]]; then
echo "indeedhub nostr-provider.js unavailable" >&2
return 1
fi
printf '%s' "$provider" | grep -Eq 'window\.nostr|nostr' || {
echo "indeedhub nostr-provider.js does not look like a Nostr signer bridge" >&2
return 1
}
pubkey=$(rpc_result node.nostr-pubkey | jq -r '.nostr_pubkey // empty')
if ! [[ "$pubkey" =~ ^[0-9a-fA-F]{64}$ ]]; then
echo "indeedhub Nostr signer pubkey unavailable: $pubkey" >&2
return 1
fi
now=$(date +%s)
signed=$(rpc_result node.nostr-sign "$(jq -nc --argjson created_at "$now" '{event:{kind:1,created_at:$created_at,tags:[],content:"archy lifecycle indeedhub signer probe"}}')")
printf '%s' "$signed" | jq -e --arg pubkey "$pubkey" '
.pubkey == $pubkey and
(.id | type == "string" and test("^[0-9a-f]{64}$")) and
(.sig | type == "string" and test("^[0-9a-f]{128}$")) and
.content == "archy lifecycle indeedhub signer probe"
' >/dev/null || {
echo "indeedhub Nostr signer did not return a valid signed event: $signed" >&2
return 1
}
}
probe_tailscale_login_ui() {
local body="$1"
if grep -Eiq 'tailscale|login|log in|sign in|authenticate|authorize|auth key|connect' "$body"; then
return 0
fi
echo "tailscale launch did not present login/auth UI content" >&2
return 1
}
install_app() {
local app="$1" app_json image params
app_json=$(catalog_app_json "$app" || true)
if [[ -n "$app_json" ]]; then
params=$(printf '%s' "$app_json" | jq -c '{id, dockerImage, version, containerConfig} | with_entries(select(.value != null))')
else
image=$(image_for "$app")
params=$(jq -nc --arg id "$app" --arg img "$image" '{id:$id,dockerImage:$img,version:"latest"}')
fi
rpc_result package.install "$params" >/dev/null
}
expect_archival_blocked_install() {
local app="$1" app_json resp err params
app_json=$(catalog_app_json "$app")
params=$(printf '%s' "$app_json" | jq -c '{id, dockerImage, version, containerConfig} | with_entries(select(.value != null))')
resp=$(rpc_call package.install "$params")
err=$(printf '%s' "$resp" | jq -r '.error.message // empty')
if [[ "$err" != *"Requires an archival Bitcoin node"* && "$err" != *"requires an archival Bitcoin node"* && "$err" != *"running pruned Bitcoin"* ]]; then
echo "expected archival Bitcoin block for $app, got: $resp" >&2
return 1
fi
}
start_app() { rpc_result package.start "$(jq -nc --arg id "$1" '{id:$id}')" >/dev/null; }
stop_app() { rpc_result package.stop "$(jq -nc --arg id "$1" '{id:$id}')" >/dev/null; }
restart_app() { rpc_result package.restart "$(jq -nc --arg id "$1" '{id:$id}')" >/dev/null; }
uninstall_app() { rpc_result package.uninstall "$(jq -nc --arg id "$1" '{id:$id,preserve_data:true}')" >/dev/null; }
audit_app() {
local app="$1" state rc=0
state=$(container_state "$app" || echo unknown)
printf '%-22s state=%s\n' "$app" "$state"
case "$state" in
absent) ;;
running)
wait_container_healthy "$app" || rc=1
wait_launch "$app" || rc=1
assert_launch_metadata "$app" || rc=1
observe_stable "$app" || rc=1
;;
*) echo "bad state: $app is $state" >&2; rc=1 ;;
esac
return "$rc"
}
full_lifecycle_app() {
local app="$1"
if [[ "$app" == "bitcoin-core" && "$ARCHY_ALLOW_BITCOIN_SWAP" != "1" ]]; then
echo "skip bitcoin-core: set ARCHY_ALLOW_BITCOIN_SWAP=1 to test mutually-exclusive Bitcoin implementation"
return 0
fi
if app_in_list "$app" "${ARCHIVAL_ONLY_APPS[@]}" && is_pruned_node; then
echo "== $app: expect archival Bitcoin block =="
expect_archival_blocked_install "$app"
return $?
fi
echo "== $app: install =="
install_app "$app" || return 1
wait_not_installing "$app" || return 1
wait_state "$app" running || return 1
wait_container_healthy "$app" || return 1
wait_launch "$app" || return 1
assert_launch_metadata "$app" || return 1
observe_stable "$app" || return 1
echo "== $app: stop =="
stop_app "$app" || return 1
wait_state "$app" stopped 300 || return 1
echo "== $app: start =="
start_app "$app" || return 1
wait_state "$app" running || return 1
wait_container_healthy "$app" || return 1
wait_launch "$app" || return 1
assert_launch_metadata "$app" || return 1
observe_stable "$app" || return 1
echo "== $app: restart =="
restart_app "$app" || return 1
wait_state "$app" running || return 1
wait_container_healthy "$app" || return 1
wait_launch "$app" || return 1
assert_launch_metadata "$app" || return 1
observe_stable "$app" || return 1
echo "== $app: uninstall preserve_data =="
uninstall_app "$app" || return 1
wait_absent_settled "$app" 600 || return 1
echo "== $app: reinstall =="
install_app "$app" || return 1
wait_not_installing "$app" || return 1
wait_state "$app" running || return 1
wait_container_healthy "$app" || return 1
wait_launch "$app" || return 1
assert_launch_metadata "$app" || return 1
observe_stable "$app" || return 1
}
apps=()
if [[ -n "$ARCHY_APPS" ]]; then
IFS=',' read -r -a apps <<< "$ARCHY_APPS"
fetch_catalog || true
elif [[ "$ARCHY_FULL_LIFECYCLE" == "1" ]]; then
fetch_catalog
mapfile -t apps < <(catalog_app_ids)
else
if fetch_catalog; then
mapfile -t apps < <(catalog_app_ids)
else
apps=("${ALL_APPS[@]}")
fi
fi
rpc_login
failed=0
for i in $(seq 1 "$ARCHY_ITERATIONS"); do
echo "### $ARCHY_HOST iteration $i / $ARCHY_ITERATIONS ###"
for app in "${apps[@]}"; do
if [[ "$ARCHY_FULL_LIFECYCLE" == "1" ]]; then
full_lifecycle_app "$app" || failed=$((failed + 1))
else
audit_app "$app" || failed=$((failed + 1))
fi
done
done
if (( failed > 0 )); then
echo "FAILED checks: $failed" >&2
exit 1
fi
echo "all checks passed"