#!/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"