156 lines
8.0 KiB
Bash
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.'
|