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

167 lines
9.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Trusted control-plane script: invoke this from an inspected harness checkout,
# NEVER from an untrusted proposal. ARCHY_TEST_REPO is a disposable source export.
# Compilation and execution both have no host credentials, engine socket,
# service storage, home or host configuration mounts. No host cargo execution.
set -euo pipefail
CONTROL=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
[[ ${ARCHY_TEST_ISOLATOR:-} == podman ]] || { echo 'Cloud tests require isolated Podman compilation' >&2; exit 2; }
[[ ${ARCHY_TEST_PACKAGE:-archipelago} == archipelago ]] || { echo 'Cloud fixture supports only the backend test target' >&2; exit 2; }
REPO=$(realpath "${ARCHY_TEST_REPO:?Disposable exact source export required}")
[[ -d "$REPO/core" && ! -e "$REPO/.git" && ! -L "$REPO/.git" ]] || { echo 'Use a disposable source export, not a live worktree' >&2; exit 2; }
image=${ARCHY_TEST_IMAGE:?Immutable local compiler image required}
[[ "$image" =~ ^(sha256:)?[a-f0-9]{64}$ ]] || { echo 'Compiler image must be an immutable local ID' >&2; exit 2; }
runtime_image=${ARCHY_TEST_CLOUD_RUNTIME_IMAGE:-$image}
[[ "$runtime_image" =~ ^(sha256:)?[a-f0-9]{64}$ ]] || { echo 'Runtime image must be an immutable local ID' >&2; exit 2; }
podman image exists "$runtime_image" || { echo 'Runtime image is not locally available' >&2; exit 2; }
export_dir=${ARCHY_TEST_ARTIFACT_EXPORT:-}
if [[ -n "$export_dir" ]]; then
export_dir=$(realpath -e "$export_dir")
[[ -d "$export_dir" && -z "$(find "$export_dir" -mindepth 1 -maxdepth 1 -print -quit)" ]] || {
echo 'Artifact export requires an existing empty directory' >&2; exit 2;
}
fi
cache=$(realpath "${ARCHY_TEST_CARGO_HOME:?Disposable public dependency cache required}")
[[ -d "$cache/registry" ]] || { echo 'Offline public registry cache is missing' >&2; exit 2; }
# Only public registry data is mounted, read-only. Never mount cargo credentials,
# config, binaries or git checkouts. The caller must establish cache provenance.
upstream=${ARCHY_TEST_CLOUD_UPSTREAM:-nextcloud29}
case "$upstream" in nextcloud29) runtime_seconds=120 ;; immich274) runtime_seconds=180 ;; *) echo 'Unknown Cloud upstream' >&2; exit 2 ;; esac
compile_only=${ARCHY_TEST_CLOUD_COMPILE_ONLY:-0}
reuse=${ARCHY_TEST_REUSE_ARTIFACT:-}
[[ "$compile_only" == 0 || "$compile_only" == 1 ]] || exit 2
if [[ "$compile_only" == 1 ]]; then
[[ -n "$export_dir" && -z "$reuse" ]] || { echo 'Compile-only requires a fresh artifact export' >&2; exit 2; }
else
: "${ARCHY_TEST_CLOUD_FIXTURE_CONTAINER:?Fixture required}"
: "${ARCHY_TEST_CLOUD_FIXTURE_IMAGE:?Immutable fixture image required}"
fi
if [[ -n "$reuse" ]]; then
[[ -z "$export_dir" ]] || { echo 'Reuse cannot overwrite an export' >&2; exit 2; }
reuse=$(realpath -e "$reuse")
fi
artifacts=$(mktemp -d /tmp/archy-cloud-test.XXXXXXXX)
containers=()
cleanup() {
for cid in "${containers[@]}"; do podman rm -f "$cid" >/dev/null 2>&1 || true; done
rm -rf -- "$artifacts"
}
trap cleanup EXIT
: > "$artifacts/test-binary"
: > "$artifacts/empty-resolv.conf"
# This host-only synthetic file is deliberately outside every bind mount.
: > "$artifacts/host-only-canary"
new_container() {
local cid
cid=$(podman create "$@")
[[ "$cid" =~ ^[a-f0-9]{64}$ ]] || { echo 'Invalid created test container identity' >&2; exit 2; }
containers+=("$cid")
CREATED=$cid
}
run_bounded() {
local seconds=$1 cid=$2
timeout --signal=TERM --kill-after=10s "$seconds" podman start --attach "$cid" 2>&1 |
python3 "$CONTROL/scripts/limit-test-output.py"
}
if [[ -n "$reuse" ]]; then
python3 "$CONTROL/scripts/cloud-test-artifact.py" reuse "$REPO" "$image" "$runtime_image" "$artifacts/test-binary" "$reuse" v1
else
new_container --name "archy-cloud-compile-${artifacts##*.}" --pull=never \
--network=none --pid=private --ipc=private --no-hosts --hostname cloud-compile \
--http-proxy=false --log-driver=none --timeout=1800 \
--cpus=2 --memory=8g --memory-swap=8g --pids-limit=512 \
--ulimit fsize=1073741824:1073741824 \
--cap-drop=all --security-opt=no-new-privileges --read-only \
--tmpfs /build:rw,size=4g --tmpfs /tmp:rw,size=512m --tmpfs /root:rw,size=64m \
--tmpfs /cargo-home:rw,size=16m \
--volume "$cache/registry:/cargo-home/registry:ro" \
--volume "$REPO:/workspace:ro" \
--volume "$artifacts/test-binary:/artifacts/test-binary:rw" \
--volume "$artifacts/empty-resolv.conf:/etc/resolv.conf:ro" \
--env CARGO_HOME=/cargo-home --env CARGO_TARGET_DIR=/build \
--env CARGO_NET_OFFLINE=true --env CARGO_BUILD_JOBS=2 --env CARGO_INCREMENTAL=0 --env HOME=/root \
--workdir /workspace --entrypoint /bin/bash "$image" -ec '
result=0
cargo test --manifest-path core/Cargo.toml -p archipelago --bin archipelago \
--offline --locked --no-run --message-format=json \
--config profile.test.debug=0 --config profile.test.package.archipelago.opt-level=0 \
--config profile.test.package.archipelago.codegen-units=16 \
--config profile.test.package.archipelago.debug=0 > /tmp/metadata || result=$?
python3 - <<"PY"
from pathlib import Path
for name in ("memory.peak", "memory.events", "memory.max", "memory.swap.max"):
path = Path("/sys/fs/cgroup") / name
print("Compiler cgroup " + name + ": " + (path.read_text().strip() if path.exists() else "unavailable"))
PY
[ "$result" = 0 ] || exit "$result"
python3 - <<"PY"
import json, pathlib, shutil
executables=[]
for line in open("/tmp/metadata"):
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"):
executables.append(item["executable"])
assert len(executables)==1, "Expected one test executable"
source=pathlib.Path(executables[0]).resolve(strict=True)
assert source.is_relative_to("/build") and source.is_file(), "Executable escaped build scratch"
shutil.copyfile(source,"/artifacts/test-binary")
PY
'
run_bounded 1820 "$CREATED"
fi
[[ -s "$artifacts/test-binary" ]] || { echo 'Compiler produced no test artifact' >&2; exit 2; }
chmod 0500 "$artifacts/test-binary"
echo 'Isolated test artifact SHA256:'
sha256sum "$artifacts/test-binary" | cut -d' ' -f1
# Preserve a source/compiler-bound artifact BEFORE execution, including on test
# failure. This is evidence, not permission to reuse against different inputs.
if [[ -n "$export_dir" ]]; then
python3 "$CONTROL/scripts/cloud-test-artifact.py" export "$REPO" "$image" "$runtime_image" "$artifacts/test-binary" "$export_dir" v1
fi
[[ "$compile_only" == 0 ]] || { echo 'Compile-only artifact retained; no fixture or test execution performed'; exit 0; }
fixture=${ARCHY_TEST_CLOUD_FIXTURE_CONTAINER:?Fixture required}
fixture_image=${ARCHY_TEST_CLOUD_FIXTURE_IMAGE:?Immutable fixture image required}
# Validate only AFTER the possibly lengthy compile. A name/label is not image
# provenance: the launcher must supply the exact reviewed disposable fixture.
if [[ "$upstream" == immich274 ]]; then
[[ "$fixture" =~ ^(archy-cloud-immich-[A-Za-z0-9]+)-db$ ]] || exit 2
prefix=${BASH_REMATCH[1]}
: "${ARCHY_TEST_IMMICH_SERVER_IMAGE:?Exact Immich image required}"
: "${ARCHY_TEST_IMMICH_REDIS_IMAGE:?Exact Redis image required}"
pids=$(podman inspect "$prefix-db" "$prefix-redis" "$prefix-server" |
python3 "$CONTROL/scripts/validate-immich-test-fixture.py" "$prefix" "$fixture_image" "$ARCHY_TEST_IMMICH_REDIS_IMAGE" "$ARCHY_TEST_IMMICH_SERVER_IMAGE")
[[ "$pids" =~ ^[0-9]+\ [0-9]+\ [0-9]+$ ]] || exit 2
else
pids=$(podman inspect "$fixture" | python3 "$CONTROL/scripts/validate-cloud-test-fixture.py" "$fixture" "$fixture_image")
fi
expected=
for pid in $pids; do
namespace=$(sudo -n readlink "/proc/$pid/ns/net")
[[ -z "$expected" || "$namespace" == "$expected" ]] || { echo 'Fixture service namespace mismatch' >&2; exit 2; }
expected=$namespace
done
[[ "$expected" =~ ^net:\[[0-9]+\]$ && "$expected" != "$(readlink /proc/self/ns/net)" ]] || {
echo 'Fixture namespace is invalid or is the host' >&2; exit 2;
}
new_container --name "archy-cloud-execute-${artifacts##*.}" --pull=never \
--network="container:$fixture" --pid=private --ipc=private --no-hosts \
--http-proxy=false --log-driver=none --timeout="$runtime_seconds" \
--cpus=2 --memory=3g --memory-swap=3g --pids-limit=256 \
--ulimit fsize=1073741824:1073741824 \
--cap-drop=all --security-opt=no-new-privileges --read-only \
--tmpfs /tmp:rw,notmpcopyup,size=128m --tmpfs /root:rw,notmpcopyup,size=16m --tmpfs /run:rw,notmpcopyup,size=16m \
--tmpfs /var/lib/archipelago:rw,notmpcopyup,size=64m --tmpfs /var/lib/containers:rw,notmpcopyup,size=16m \
--volume "$REPO:/workspace:ro" \
--volume "$artifacts/test-binary:/test-binary:ro" \
--volume "$artifacts/empty-resolv.conf:/etc/resolv.conf:ro" \
--env NODE_PATH=/browser/node_modules \
--env HOME=/root --env ARCHY_TEST_ISOLATED=1 --env "ARCHY_TEST_CLOUD_UPSTREAM=$upstream" \
--env "ARCHY_TEST_HOST_CANARY=$artifacts/host-only-canary" \
--workdir /workspace/core --entrypoint /bin/sh "$runtime_image" -ec '
[ "$(readlink /proc/self/ns/net)" = "$1" ] || { echo "Fixture namespace changed" >&2; exit 2; }
shift
exec /test-binary --test-threads=2 "$@"
' cloud-netns "$expected" "$@"
run_bounded "$((runtime_seconds + 20))" "$CREATED"