Files
archy/tests/apps/podsteadr/run-media-disposable.sh

156 lines
8.0 KiB
Bash

#!/usr/bin/env bash
# Actual packaged, money-free media composition. No host network or live data.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)
image=${PODSTEADR_FIXTURE_IMAGE:?Immutable Podsteadr image required}
mtx=${MEDIAMTX_FIXTURE_IMAGE:?Immutable MediaMTX image required}
blossom=${BLOSSOM_FIXTURE_IMAGE:?Immutable Blossom image required}
browser=${PODSTEADR_BROWSER_IMAGE:-}
bootstrap=${PODSTEADR_KEY_BOOTSTRAP:-0}
[[ "$bootstrap" == 0 || "$bootstrap" == 1 ]] || exit 2
for id in "$image" "$mtx" "$blossom" ${browser:+"$browser"}; do
[[ "$id" =~ ^(sha256:)?[a-f0-9]{64}$ ]] || exit 2
podman image exists "$id"
done
name="archy-podsteadr-media-$$"
work=$(mktemp -d)
volumes=()
containers=()
cleanup() {
result=$?
trap - EXIT
# Remove namespace consumers before the namespace owner, even on failure.
for ((i=${#containers[@]}-1; i>=0; i--)); do
c=${containers[$i]}
if ! podman container exists "$c"; then continue; fi
if [[ "$result" != 0 ]]; then podman logs --tail 45 "$c" 2>&1 || true; fi
timeout 15 podman rm -f --time 0 "$c" >/dev/null 2>&1 || true
done
for v in "${volumes[@]}"; do podman volume rm -f "$v" >/dev/null 2>&1 || true; done
for c in "${containers[@]}"; do
if podman container exists "$c"; then echo "Fixture cleanup failed: $c" >&2; result=1; fi
done
for v in "${volumes[@]}"; do
if podman volume exists "$v"; then echo "Fixture volume cleanup failed: $v" >&2; result=1; fi
done
rm -rf -- "$work"
exit "$result"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
[[ $(podman image inspect "$image" --format '{{.Config.User}}') == 1000:1000 ]]
[[ $(podman image inspect "$blossom" --format '{{.Config.User}}') == 1000:1000 ]]
common=(--pull=never --pid=private --ipc=private --http-proxy=false --no-hosts
--read-only --cap-drop=all --security-opt=no-new-privileges --cpus=1
--memory=512m --memory-swap=512m --pids-limit=128 --timeout=360
--tmpfs /tmp:rw,notmpcopyup,nosuid,nodev,size=64m)
printf '{}\n' > "$work/config.json"
printf '// Disposable fixture: no signer bridge injected.\n' > "$work/nostr-provider.js"
: > "$work/resolv.conf"
common+=(--volume "$work/resolv.conf:/etc/resolv.conf:ro")
containers+=("$name-key")
pubkey=$(podman run --rm --name "$name-key" "${common[@]}" --network=none --entrypoint node "$image" --input-type=module -e \
'import {getPublicKey} from "nostr-tools/pure";console.log(getPublicKey(new Uint8Array(32).fill(1)))')
[[ "$pubkey" =~ ^[a-f0-9]{64}$ ]]
for suffix in data recordings blossom; do
volumes+=("$name-$suffix")
podman volume create "$name-$suffix" >/dev/null
done
if [[ -n "$browser" ]]; then
# Browser runtime has Playwright, not Podsteadr's signer dependency. Reuse only
# public dependency bytes from the pinned image in a disposable named volume.
volumes+=("$name-signer")
podman volume create "$name-signer" >/dev/null
containers+=("$name-signer-init" "$name-browser-preflight")
podman run --rm --name "$name-signer-init" "${common[@]}" --network=none \
--volume "$name-signer:/signer:U" --entrypoint /bin/sh "$image" -ec \
'mkdir /signer/node_modules; cp -a /app/node_modules/. /signer/node_modules/'
podman run --rm --name "$name-browser-preflight" "${common[@]}" --network=none \
--volume "$name-signer:/signer:ro" --entrypoint node "$browser" -e \
'require("@playwright/test");const n=require("module").createRequire("/signer/package.json")("nostr-tools/pure"); if(n.getPublicKey(new Uint8Array(32).fill(1)).length!==64)process.exit(1)'
fi
# Stable namespace owner lets all three services be recreated without network drift.
containers+=("$name-net")
podman run -d --name "$name-net" "${common[@]}" --memory=64m --memory-swap=64m --network=none \
--entrypoint node "$image" -e 'process.on("SIGTERM",()=>process.exit(0));setInterval(()=>{},10000)' >/dev/null
common+=(--network="container:$name-net")
containers+=("$name-app" "$name-mtx" "$name-blossom" "$name-bootstrap")
start_app() {
podman run -d --name "$name-app" "${common[@]}" \
--volume "$name-data:/data:U" --volume "$name-recordings:/recordings:ro,U" \
--volume "$repo/tests/apps/podsteadr/media.mjs:/app/qualification-media.mjs:ro" \
--env "PODSTEADR_ADMIN_PUBKEYS=$pubkey" --env PUBLIC_URL=http://127.0.0.1:8095 \
--env NOSTR_RELAYS= --env CASHU_MINT_URL_DEFAULT=http://127.0.0.1:9996 \
--env MEDIAMTX_API_URL=http://127.0.0.1:9997 \
--env MEDIAMTX_RTMP_PUBLIC=rtmp://127.0.0.1:1935 --env MEDIAMTX_HLS_PUBLIC=http://127.0.0.1:8888 \
--env MEDIAMTX_WHIP_PUBLIC=http://127.0.0.1:8889 \
--env BLOSSOM_URL_DEFAULT=http://127.0.0.1:3000 "$image" >/dev/null
ready http://127.0.0.1:8095/api/health
}
ready() {
for _ in $(seq 1 40); do
if timeout 3 podman exec "$name-net" node -e \
'fetch(process.argv[1],{signal:AbortSignal.timeout(1500)}).then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))' "$1" >/dev/null 2>&1; then return; fi
sleep 0.25
done
echo "Readiness failed: $1" >&2; return 1
}
start_media() {
podman run -d --name "$name-mtx" "${common[@]}" --user=1000:1000 \
--volume "$repo/tests/apps/podsteadr/mediamtx-fixture.yml:/mediamtx.yml:ro" \
--volume "$name-recordings:/recordings" "$mtx" >/dev/null
podman run -d --name "$name-blossom" "${common[@]}" \
--volume "$name-blossom:/data:U" --volume "$work/config.json:/config/config.json:ro" \
--volume "$work/nostr-provider.js:/bridge/nostr-provider.js:ro" \
--env "ARCHY_BLOSSOM_PUBKEYS=$serverpubkey" "$blossom" >/dev/null
ready http://127.0.0.1:9997/v3/paths/list
ready http://127.0.0.1:3000/healthz
}
if [[ "$bootstrap" == 1 ]]; then
# Break the dependency cycle without starting HTTP/relay/database services or
# exporting a private key. Only disposable fixture data is initialized here.
podman run --rm --name "$name-bootstrap" "${common[@]}" \
--volume "$name-data:/data:U" --entrypoint node "$image" \
dist/provision-server-key.js /data > "$work/bootstrap.json"
serverpubkey=$(python3 - "$work/bootstrap.json" <<'PY'
import json,re,sys
value=json.load(open(sys.argv[1]))
assert set(value)=={'version','publicKey'} and value['version']==1
assert re.fullmatch('[a-f0-9]{64}',value['publicKey'])
print(value['publicKey'])
PY
)
start_media
start_app
podman exec --env "EXPECTED_KEY=$serverpubkey" "$name-app" node --input-type=module -e '
const response=await fetch("http://127.0.0.1:8095/api/settings/public");
if(!response.ok || (await response.json()).serverPubkey!==process.env.EXPECTED_KEY) process.exit(1);
'
else
start_app
serverpubkey=$(podman exec "$name-app" node -e 'fetch("http://127.0.0.1:8095/api/settings/public").then(r=>r.json()).then(s=>console.log(s.serverPubkey))')
[[ "$serverpubkey" =~ ^[a-f0-9]{64}$ ]]
start_media
fi
timeout --kill-after=5s 150 podman exec --env PODSTEADR_MEDIA_PHASE=seed "$name-app" node /app/qualification-media.mjs
if [[ -n "$browser" ]]; then
containers+=("$name-browser")
timeout --kill-after=5s 90 podman run --rm --name "$name-browser" "${common[@]}" \
--memory=1g --memory-swap=1g --pids-limit=256 \
--tmpfs /root:rw,notmpcopyup,nosuid,nodev,size=16m \
--volume "$repo/tests/apps/podsteadr/media-browser.mjs:/browser/media-browser.mjs:ro" \
--volume "$name-signer:/signer:ro" \
--entrypoint node "$browser" /browser/media-browser.mjs
fi
for c in "$name-app" "$name-mtx" "$name-blossom"; do
podman stop --time 5 "$c" >/dev/null
[[ $(podman inspect "$c" --format '{{.State.ExitCode}}') == 0 ]] || { echo "Unclean service shutdown: $c" >&2; exit 1; }
podman rm "$c" >/dev/null
done
if [[ "$bootstrap" == 1 ]]; then start_media; start_app; else start_app; start_media; fi
timeout --kill-after=5s 45 podman exec --env PODSTEADR_MEDIA_PHASE=verify \
--env "PODSTEADR_MEDIA_BROWSER=${browser:+1}" "$name-app" node /app/qualification-media.mjs
if [[ "$bootstrap" == 1 ]]; then echo 'PASS key-only bootstrap before sidecars before app; service signer upload and recreation retain exact key'; fi
echo 'PASS actual packaged Podsteadr/MediaMTX/Blossom composition and recreation; not manager lifecycle, real identity/relay/payment, WHIP/device or release provenance acceptance.'