Files
archy/scripts/test-backend-isolated.sh
T

180 lines
8.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Compile normally; execute unit tests away from real wallets, service buses,
# container storage, processes and networking. Never silently fall back to host.
set -euo pipefail
SCRIPT_REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
REPO=${ARCHY_TEST_REPO:-$SCRIPT_REPO}
REPO=$(cd "$REPO" && pwd)
if [[ ${ARCHY_TEST_PACKAGE:-archipelago} == gashboard ]]; then
[[ $# == 0 ]] || { echo 'Gashboard isolation accepts no command arguments' >&2; exit 2; }
[[ -z ${ARCHY_TEST_TERMINAL_FIXTURE:-}${ARCHY_TEST_CLOUD_FIXTURE_CONTAINER:-} &&
${ARCHY_TEST_C01_STARTUP:-0} == 0 && ${ARCHY_TEST_CLOUD_COMPILE_ONLY:-0} == 0 &&
${ARCHY_TEST_RADIO_OWNER:-0} == 0 ]] || {
echo 'Gashboard isolation cannot be combined with another fixture mode' >&2; exit 2;
}
exec bash "$SCRIPT_REPO/scripts/test-gashboard-isolated.sh" "$REPO"
fi
if [[ ${ARCHY_TEST_C01_STARTUP:-0} == 1 && ${ARCHY_TEST_ISOLATOR:-systemd} != systemd ]]; then
echo 'C01 startup qualification requires the systemd fixture namespace' >&2; exit 2
fi
if [[ -n ${ARCHY_TEST_TERMINAL_FIXTURE:-} ]]; then
exec bash "$SCRIPT_REPO/scripts/test-terminal-backend-isolated.sh" "$@"
fi
if [[ -n ${ARCHY_TEST_CLOUD_FIXTURE_CONTAINER:-} || ${ARCHY_TEST_CLOUD_COMPILE_ONLY:-0} == 1 ]]; then
[[ ${ARCHY_TEST_ISOLATOR:-systemd} == podman ]] || {
echo 'Cloud fixture tests require Podman-isolated compilation; host compilation is forbidden' >&2; exit 2;
}
exec bash "$SCRIPT_REPO/scripts/test-cloud-backend-isolated.sh" "$@"
fi
case "${ARCHY_TEST_PACKAGE:-archipelago}" in
archipelago) test_target=(-p archipelago --bin archipelago) ;;
archipelago-publishing-tests) test_target=(-p archipelago-publishing-tests --lib) ;;
archipelago-container) test_target=(-p archipelago-container --lib) ;;
archipelago-security) test_target=(-p archipelago-security --lib) ;;
*) echo 'Unsupported isolated test package' >&2; exit 2 ;;
esac
if [[ ${ARCHY_TEST_ISOLATOR:-systemd} == podman ]]; then
command -v podman >/dev/null
image=${ARCHY_TEST_IMAGE:?ARCHY_TEST_IMAGE is required for podman isolation}
cargo_home=${ARCHY_TEST_CARGO_HOME:?ARCHY_TEST_CARGO_HOME is required for podman isolation}
cargo_home=$(mkdir -p "$cargo_home" && cd "$cargo_home" && pwd)
artifacts=$(mktemp -d)
trap 'rm -rf -- "$artifacts"' EXIT
install -d "$artifacts/runtime/archipelago" "$artifacts/runtime/containers" \
"$artifacts/runtime/tmp"
podman run --rm \
--cpus="${ARCHY_TEST_CPUS:-4}" --memory="${ARCHY_TEST_MEMORY:-4g}" --pids-limit=2048 \
--cap-drop=all --security-opt=no-new-privileges --read-only \
--tmpfs /tmp:rw,size=512m --tmpfs /root:rw,size=512m \
--network=pasta --env CARGO_HOME=/cargo-home \
--volume "$cargo_home:/cargo-home:rw,Z" \
--volume "$REPO:/workspace:rw,Z" --volume "$artifacts:/artifacts:rw,Z" \
--workdir /workspace \
"$image" \
cargo test --manifest-path core/Cargo.toml "${test_target[@]}" \
--locked --no-run --message-format=json \
--config 'profile.test.package.archipelago.opt-level=0' \
--config 'profile.test.package.archipelago.debug=0' \
>"$artifacts/metadata"
executable=$(python3 - "$artifacts/metadata" <<'PY'
import json,sys
found=[]
for line in open(sys.argv[1]):
try: item=json.loads(line)
except json.JSONDecodeError: continue
if item.get('reason')=='compiler-artifact' and item.get('profile',{}).get('test') and item.get('executable'):
found.append(item['executable'])
assert len(found)==1, f'Expected one unit test executable, got {len(found)}'
print(found[0])
PY
)
case "$executable" in
/workspace/*) ;;
*) echo 'Compiled test executable escaped the workspace' >&2; exit 1 ;;
esac
podman run --rm \
--cpus="${ARCHY_TEST_CPUS:-4}" --memory="${ARCHY_TEST_MEMORY:-4g}" --pids-limit=1024 \
--cap-drop=all --cap-add=chown --cap-add=fowner --cap-add=setuid --cap-add=setgid \
--security-opt=no-new-privileges --read-only \
--tmpfs /run:rw,size=64m --tmpfs /root:rw,size=64m \
--tmpfs /var/lib/tor:rw,size=64m --tmpfs /tmp:rw,size=2g --network=none \
--tmpfs /var/lib/archipelago-public-updates:rw --tmpfs /var/lib/archipelago-public-transactions:rw \
--volume "$artifacts/runtime/archipelago:/var/lib/archipelago:rw,Z" \
--volume "$artifacts/runtime/containers:/var/lib/containers:rw,Z" \
--volume "$REPO:/workspace:ro,Z" --workdir /workspace/core \
--env ARCHY_TEST_ISOLATED=1 \
--env ARCHY_TOR_OWNER_FIXTURE=1 \
"$image" "$executable" --test-threads="${ARCHY_TEST_THREADS:-4}" "$@"
exit
fi
[[ ${ARCHY_TEST_ISOLATOR:-systemd} == systemd ]] || {
echo 'ARCHY_TEST_ISOLATOR must be systemd or podman' >&2
exit 2
}
# Fresh tmpfs mounts isolate both data and free-space accounting. PrivateTmp=yes
# would overlay these with private directories on the potentially full host /tmp.
test_filesystems='/run:rw /var/lib/archipelago:rw /var/lib/containers:rw /root:rw /var/lib/tor:rw,size=64m /tmp:rw,size=2G,mode=1777 /var/tmp:rw,size=256M,mode=1777'
test_filesystems+=' /var/lib/archipelago-public-updates:rw /var/lib/archipelago-public-transactions:rw'
startup_environment=()
if [[ ${ARCHY_TEST_LOGIN_PASSWORD:-0} == 1 ]]; then
# Password owner qualification gets empty account files, never host shadow.
test_filesystems+=' /etc:rw,size=16M,mode=755'
startup_environment+=(--setenv=ARCHY_TEST_LOGIN_PASSWORD=1)
fi
if [[ ${ARCHY_TEST_C01_STARTUP:-0} == 1 ]]; then
[[ -n ${ARCHY_TEST_C01_BINARY:-} && -x $ARCHY_TEST_C01_BINARY ]] || {
echo 'C01 startup qualification requires a freshly compiled non-test binary' >&2; exit 2;
}
startup_binary=$(realpath -- "$ARCHY_TEST_C01_BINARY")
# Hide installed configuration/payloads as well as the ordinary test data.
# These are disposable fixture mounts, never C01 installation authority.
test_filesystems+=' /opt/archipelago:rw /etc/archipelago:rw'
startup_environment+=(--setenv=ARCHY_TEST_C01_STARTUP=1 "--setenv=ARCHY_TEST_C01_BINARY=$startup_binary")
fi
command -v systemd-run >/dev/null
command -v unshare >/dev/null
command -v setpriv >/dev/null
sudo -n true || { echo 'Isolated backend tests require noninteractive sudo for systemd namespaces.' >&2; exit 1; }
metadata=$(mktemp)
trap 'rm -f "$metadata"' EXIT
if ! cargo test --manifest-path "$REPO/core/Cargo.toml" "${test_target[@]}" \
--locked --no-run --message-format=json --config 'profile.test.package.archipelago.opt-level=0' --config 'profile.test.package.archipelago.debug=0' > "$metadata"; then
python3 - "$metadata" <<'PYDIAG'
import json,sys
for line in open(sys.argv[1]):
try: item=json.loads(line)
except json.JSONDecodeError: continue
rendered=item.get('message',{}).get('rendered') if item.get('reason')=='compiler-message' else None
if rendered: print(rendered,file=sys.stderr,end='')
PYDIAG
exit 1
fi
executable=$(python3 - "$metadata" <<'PY'
import json,sys
found=[]
for line in open(sys.argv[1]):
try: item=json.loads(line)
except json.JSONDecodeError: continue
if item.get('reason')=='compiler-artifact' and item.get('profile',{}).get('test') and item.get('executable'):
found.append(item['executable'])
assert len(found)==1, f'Expected one unit test executable, got {len(found)}'
print(found[0])
PY
)
[[ -x "$executable" ]]
radio_fixture=()
test_command=("$executable" --test-threads="${ARCHY_TEST_THREADS:-4}" "$@")
if [[ ${ARCHY_TEST_RADIO_OWNER:-0} == 1 ]]; then
test_filesystems+=' /var/lib/archipelago-radio-work:rw,size=256M,mode=700'
radio_fixture=(--property=ReadWritePaths=/sys/fs/cgroup
"--property=BindReadOnlyPaths=$executable:/opt/radio-test-client"
--setenv=ARCHY_RADIO_FIXTURE=1 --setenv=ARCHY_RADIO_CLIENT=/opt/radio-test-client)
test_command=(/usr/bin/python3 -B "$REPO/tests/regression/radio-owner.py")
fi
unit="archy-isolated-tests-$(date +%s)-$$"
execution_profile=()
if [[ ${ARCHY_TEST_PACKAGE:-archipelago} == archipelago ]]; then
execution_profile=(python3 -B "$SCRIPT_REPO/scripts/tests/with_exec_profile.py" "$REPO" --)
fi
"${execution_profile[@]}" sudo -n systemd-run --unit="$unit" --wait --pipe --collect \
--property=CPUQuota=200% --property=MemoryMax=8G --property=MemorySwapMax=0 \
--property="WorkingDirectory=$REPO/core" \
--property="BindReadOnlyPaths=$REPO" \
--property=PrivateNetwork=yes --property=PrivateTmp=no --property=PrivateDevices=yes \
--property=ProtectSystem=strict --property=ProtectHome=read-only \
--property=NoNewPrivileges=yes \
--property="TemporaryFileSystem=$test_filesystems" \
--setenv=ARCHY_TEST_ISOLATED=1 \
--setenv=ARCHY_TOR_OWNER_FIXTURE=1 \
"--setenv=RUST_MIN_STACK=${RUST_MIN_STACK:-16777216}" \
"${startup_environment[@]}" \
"${radio_fixture[@]}" \
/usr/bin/unshare --pid --fork --mount-proc --kill-child \
/usr/bin/setpriv --bounding-set=-all,+chown,+dac_override,+fowner,+setuid,+setgid,+kill \
"${test_command[@]}"