167 lines
9.1 KiB
Bash
Executable File
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"
|