Add support for sharing host directories into the VM via QEMU 9p (virtio-9p). The --mount flag can be repeated; each host directory appears at the same absolute path inside the VM. Design: - QEMU: each --mount gets a short 9p tag (slim0, slim1, …) via -virtfs local,path=…,mount_tag=slimN,security_model=mapped-xattr. Tags are kept short because 9p mount_tag has a ~31-byte limit. - Kernel cmdline: the full destination path is passed as slim.mount=<tag>:<base64(path)> so the init script knows where to mount each tag. Base64 avoids issues with spaces/special chars. - slim-init.sh: after networking, parse slim.mount= entries, mkdir -p the destination, and mount -t 9p <tag> <dest> -o trans=virtio,version=9p2000.L Tests verify: 9p share detection via sysfs mount_tag, 9p entry in mount output, file content accessible at the expected path, and clean VM exit.
313 lines
10 KiB
Bash
Executable File
313 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
|
# Test script for slim - boots containers and verifies CMD inference/override.
|
|
# Requires: podman, qemu-system-x86_64, KVM, curl, and a kernel at
|
|
# $XDG_DATA_HOME/slim-rs/registry/vmlinuz.
|
|
set -u
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
TARGET_DIR="${CARGO_TARGET_DIR:-$SCRIPT_DIR/../target}"
|
|
SLIM_BIN="$TARGET_DIR/debug/slim"
|
|
TIMEOUT=120
|
|
|
|
pass=0
|
|
fail=0
|
|
|
|
report() {
|
|
if [ "$1" = "pass" ]; then
|
|
echo " PASS: $2"
|
|
pass=$((pass + 1))
|
|
else
|
|
echo " FAIL: $2"
|
|
fail=$((fail + 1))
|
|
fi
|
|
}
|
|
|
|
check_output() {
|
|
output="$1"
|
|
marker="$2"
|
|
label="$3"
|
|
if echo "$output" | grep -q "$marker"; then
|
|
report pass "$label"
|
|
else
|
|
report fail "$label (expected marker: $marker)"
|
|
fi
|
|
}
|
|
|
|
cleanup() {
|
|
"$SLIM_BIN" image rm "$1" >/dev/null 2>&1 || true
|
|
podman image rm "$2" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
test_distro() {
|
|
distro="$1"
|
|
from="$2"
|
|
extra_setup="$3"
|
|
|
|
echo "=== Testing $distro ==="
|
|
|
|
work="$(mktemp -d)"
|
|
|
|
# === Test 1: CMD-inferred ===
|
|
echo "-- Test 1: CMD-inferred"
|
|
cat > "$work/Containerfile.infer" <<EOF
|
|
FROM $from
|
|
$extra_setup
|
|
CMD ["/bin/sh", "-c", "echo CMD_INFERRED_OK; wget -q -O /dev/null http://1.1.1.1 2>/dev/null && echo CONN_OK; wget -q -O /dev/null http://example.org 2>/dev/null && echo DNS_OK; poweroff -f"]
|
|
EOF
|
|
img="slim-test-$distro-infer"
|
|
if podman build -t "$img" -f "$work/Containerfile.infer" >/dev/null 2>&1; then
|
|
"$SLIM_BIN" build qcow2 "$img" >/dev/null 2>&1
|
|
output=$(timeout "$TIMEOUT" "$SLIM_BIN" run "$img" 2>&1 || true)
|
|
check_output "$output" "CMD_INFERRED_OK" "$distro CMD-inferred"
|
|
check_output "$output" "CONN_OK" "$distro connect to 1.1.1.1"
|
|
check_output "$output" "DNS_OK" "$distro DNS lookup"
|
|
else
|
|
report fail "$distro CMD-inferred (podman build failed)"
|
|
fi
|
|
cleanup "$img" "$img"
|
|
|
|
# === Test 2: slim run --cmd override ===
|
|
echo "-- Test 2: slim run --cmd override"
|
|
cat > "$work/Containerfile.run" <<EOF
|
|
FROM $from
|
|
$extra_setup
|
|
CMD ["/bin/sh", "-c", "echo SHOULD_NOT_APPEAR; poweroff -f"]
|
|
EOF
|
|
img="slim-test-$distro-run"
|
|
if podman build -t "$img" -f "$work/Containerfile.run" >/dev/null 2>&1; then
|
|
"$SLIM_BIN" build qcow2 "$img" >/dev/null 2>&1
|
|
output=$(timeout "$TIMEOUT" "$SLIM_BIN" run "$img" --cmd 'echo RUN_OVERRIDE_OK; poweroff -f' 2>&1 || true)
|
|
check_output "$output" "RUN_OVERRIDE_OK" "$distro run --cmd override"
|
|
else
|
|
report fail "$distro run --cmd override (podman build failed)"
|
|
fi
|
|
cleanup "$img" "$img"
|
|
|
|
# === Test 3: slim build --cmd override ===
|
|
echo "-- Test 3: slim build --cmd override"
|
|
cat > "$work/Containerfile.build" <<EOF
|
|
FROM $from
|
|
$extra_setup
|
|
CMD ["/bin/sh", "-c", "echo SHOULD_NOT_APPEAR; poweroff -f"]
|
|
EOF
|
|
img="slim-test-$distro-build"
|
|
if podman build -t "$img" -f "$work/Containerfile.build" >/dev/null 2>&1; then
|
|
"$SLIM_BIN" build qcow2 "$img" --cmd 'echo BUILD_OVERRIDE_OK; poweroff -f' >/dev/null 2>&1
|
|
output=$(timeout "$TIMEOUT" "$SLIM_BIN" run "$img" 2>&1 || true)
|
|
check_output "$output" "BUILD_OVERRIDE_OK" "$distro build --cmd override"
|
|
else
|
|
report fail "$distro build --cmd override (podman build failed)"
|
|
fi
|
|
cleanup "$img" "$img"
|
|
|
|
rm -rf "$work"
|
|
}
|
|
|
|
test_user() {
|
|
echo "=== Testing USER directive ==="
|
|
|
|
work="$(mktemp -d)"
|
|
img="slim-test-user"
|
|
|
|
cat > "$work/Containerfile" <<'EOF'
|
|
FROM alpine:latest
|
|
RUN adduser -D -u 1500 testuser
|
|
USER testuser
|
|
CMD ["/bin/sh", "-c", "echo USER_BUILD_OK:$(id -u):$(whoami); poweroff -f"]
|
|
EOF
|
|
|
|
echo "-- Building container image with USER directive..."
|
|
if ! podman build --network=none -t "$img" -f "$work/Containerfile" >/dev/null 2>&1; then
|
|
report fail "USER directive (podman build failed)"
|
|
rm -rf "$work"
|
|
return
|
|
fi
|
|
|
|
echo "-- Test 1: build-time CMD runs as USER"
|
|
"$SLIM_BIN" build qcow2 "$img" >/dev/null 2>&1
|
|
output=$(timeout "$TIMEOUT" "$SLIM_BIN" run "$img" 2>&1 || true)
|
|
check_output "$output" "USER_BUILD_OK:1500:testuser" "USER build-time CMD runs as testuser"
|
|
|
|
echo "-- Test 2: run --cmd override runs as USER"
|
|
output=$(timeout "$TIMEOUT" "$SLIM_BIN" run "$img" --cmd 'echo USER_RUN_OK:$(id -u):$(whoami); poweroff -f' 2>&1 || true)
|
|
check_output "$output" "USER_RUN_OK:1500:testuser" "USER run --cmd override runs as testuser"
|
|
|
|
cleanup "$img" "$img"
|
|
rm -rf "$work"
|
|
}
|
|
|
|
test_mount() {
|
|
echo "=== Testing --mount (9p shares) ==="
|
|
|
|
work="$(mktemp -d)"
|
|
|
|
# --- Image 1: no WORKINGDIR, test host:guest and multi-mount ---
|
|
img="slim-test-mount"
|
|
share_dir="$work/share"
|
|
share_dir2="$work/share2"
|
|
guest_dir="/mnt/guest"
|
|
guest_dir2="/mnt/guest2"
|
|
|
|
mkdir -p "$share_dir" "$share_dir2"
|
|
echo "hello from host" > "$share_dir/testfile.txt"
|
|
echo "second share" > "$share_dir2/testfile2.txt"
|
|
|
|
cat > "$work/Containerfile" <<'EOF'
|
|
FROM alpine:latest
|
|
CMD ["/bin/sh", "-c", "poweroff -f"]
|
|
EOF
|
|
|
|
echo "-- Building container image (no WORKINGDIR)..."
|
|
if ! podman build --network=none -t "$img" -f "$work/Containerfile" >/dev/null 2>&1; then
|
|
report fail "--mount (podman build failed)"
|
|
rm -rf "$work"
|
|
return
|
|
fi
|
|
|
|
echo "-- Building slim VM..."
|
|
if ! "$SLIM_BIN" build qcow2 "$img" >/dev/null 2>&1; then
|
|
report fail "--mount (slim build failed)"
|
|
cleanup "$img" "$img"
|
|
rm -rf "$work"
|
|
return
|
|
fi
|
|
|
|
share_canon=$(readlink -f "$share_dir")
|
|
share_canon2=$(readlink -f "$share_dir2")
|
|
|
|
echo "-- Test 1: --mount host:guest (absolute guest path, multi-mount)..."
|
|
output=$(timeout "$TIMEOUT" "$SLIM_BIN" run "$img" \
|
|
--mount "${share_dir}:${guest_dir}" \
|
|
--mount "${share_dir2}:${guest_dir2}" \
|
|
--cmd "for f in /sys/bus/virtio/drivers/9pnet_virtio/virtio*/mount_tag; do [ -f \"\$f\" ] && echo \"\$f: \$(tr -d '\\0' < \"\$f\")\"; done; mount -v; cat ${guest_dir}/testfile.txt 2>/dev/null || echo NO_FILE; cat ${guest_dir2}/testfile2.txt 2>/dev/null || echo NO_FILE2; echo MOUNT_VERIFY_DONE; poweroff -f" \
|
|
2>&1 || true)
|
|
|
|
check_output "$output" "mount_tag: slim0" "--mount 9p share detected"
|
|
check_output "$output" "mount_tag: slim1" "--mount second 9p share detected"
|
|
check_output "$output" "type 9p" "--mount 9p filesystem in mount list"
|
|
check_output "$output" "hello from host" "--mount file accessible at guest path"
|
|
check_output "$output" "second share" "--mount second file accessible at guest path"
|
|
check_output "$output" "MOUNT_VERIFY_DONE" "--mount VM ran to completion"
|
|
|
|
cleanup "$img" "$img"
|
|
|
|
# --- Image 2: with WORKINGDIR, test relative guest path ---
|
|
img="slim-test-mount-wd"
|
|
share_dir3="$work/share3"
|
|
|
|
mkdir -p "$share_dir3"
|
|
echo "relative share" > "$share_dir3/relfile.txt"
|
|
|
|
cat > "$work/Containerfile.wd" <<'EOF'
|
|
FROM alpine:latest
|
|
WORKDIR /app
|
|
CMD ["/bin/sh", "-c", "poweroff -f"]
|
|
EOF
|
|
|
|
echo "-- Building container image (WORKINGDIR /app)..."
|
|
if ! podman build --network=none -t "$img" -f "$work/Containerfile.wd" >/dev/null 2>&1; then
|
|
report fail "--mount relative (podman build failed)"
|
|
rm -rf "$work"
|
|
return
|
|
fi
|
|
|
|
echo "-- Building slim VM..."
|
|
if ! "$SLIM_BIN" build qcow2 "$img" >/dev/null 2>&1; then
|
|
report fail "--mount relative (slim build failed)"
|
|
cleanup "$img" "$img"
|
|
rm -rf "$work"
|
|
return
|
|
fi
|
|
|
|
echo "-- Test 2: --mount host:relative-guest (resolved against WORKINGDIR)..."
|
|
output=$(timeout "$TIMEOUT" "$SLIM_BIN" run "$img" \
|
|
--mount "${share_dir3}:data" \
|
|
--cmd "mount -v; cat /app/data/relfile.txt 2>/dev/null || echo NO_REL_FILE; echo REL_VERIFY_DONE; poweroff -f" \
|
|
2>&1 || true)
|
|
|
|
check_output "$output" "type 9p" "--mount relative 9p filesystem in mount list"
|
|
check_output "$output" "relative share" "--mount relative file accessible at WORKINGDIR/data"
|
|
check_output "$output" "REL_VERIFY_DONE" "--mount relative VM ran to completion"
|
|
|
|
cleanup "$img" "$img"
|
|
rm -rf "$work"
|
|
}
|
|
|
|
test_service() {
|
|
echo "=== Testing nextcloud service ==="
|
|
|
|
img="slim-test-nextcloud"
|
|
host_port=18080
|
|
work="$(mktemp -d)"
|
|
|
|
cat > "$work/Containerfile" <<'EOF'
|
|
FROM docker.io/library/nextcloud:32-apache
|
|
RUN apt-get update && apt-get install -y --no-install-recommends iproute2 && rm -rf /var/lib/apt/lists/*
|
|
EOF
|
|
|
|
echo "-- Building nextcloud container image..."
|
|
if ! podman build -t "$img" -f "$work/Containerfile" >/dev/null 2>&1; then
|
|
report fail "nextcloud service (podman build failed)"
|
|
rm -rf "$work"
|
|
return
|
|
fi
|
|
|
|
echo "-- Building slim VM..."
|
|
if ! "$SLIM_BIN" build qcow2 "$img" >/dev/null 2>&1; then
|
|
report fail "nextcloud service (slim build failed)"
|
|
cleanup "$img" "$img"
|
|
rm -rf "$work"
|
|
return
|
|
fi
|
|
|
|
echo "-- Booting VM with port forward (host :${host_port} -> guest :80)..."
|
|
setsid timeout 300 "$SLIM_BIN" run "$img" \
|
|
--forward "tcp:0.0.0.0:${host_port}-:80" \
|
|
--memory 2048M >/dev/null 2>&1 &
|
|
vm_pid=$!
|
|
|
|
echo "-- Waiting for Nextcloud to start..."
|
|
up=0
|
|
for _ in $(seq 1 60); do
|
|
if curl -s -o /dev/null -m 2 "http://localhost:${host_port}/" 2>/dev/null; then
|
|
up=1
|
|
break
|
|
fi
|
|
sleep 3
|
|
done
|
|
|
|
if [ "$up" = "1" ]; then
|
|
response=$(curl -s -L -m 10 "http://localhost:${host_port}/" 2>/dev/null || true)
|
|
if echo "$response" | grep -qi "nextcloud"; then
|
|
report pass "nextcloud service (port forward + content)"
|
|
else
|
|
report fail "nextcloud service (port reachable but no 'nextcloud' in response)"
|
|
fi
|
|
else
|
|
report fail "nextcloud service (port not reachable within timeout)"
|
|
fi
|
|
|
|
pkill -f "qemu-system-x86_64.*$img" 2>/dev/null || true
|
|
kill "$vm_pid" 2>/dev/null || true
|
|
wait "$vm_pid" 2>/dev/null || true
|
|
|
|
cleanup "$img" "$img"
|
|
rm -rf "$work"
|
|
}
|
|
|
|
echo "Building slim..."
|
|
cargo build 2>&1
|
|
|
|
test_distro "alpine" "alpine:latest" ""
|
|
test_distro "archlinux" "archlinux:latest" "RUN pacman -Sy --noconfirm iproute2 wget; pacman -Sc --noconfirm"
|
|
|
|
test_user
|
|
|
|
test_mount
|
|
|
|
test_service
|
|
|
|
echo ""
|
|
echo "=== Results: $pass passed, $fail failed ==="
|
|
[ "$fail" -eq 0 ]
|