#!/usr/bin/env bash set -euo pipefail image="${IMAGE:?IMAGE is required}" work_dir="${WORK_DIR:?WORK_DIR is required}" port="${PORT:-18088}" duration_seconds="${DURATION_SECONDS:-600}" request_interval="${REQUEST_INTERVAL:-5}" cpu_limit="${CPU_LIMIT:-0.5}" memory_limit="${MEMORY_LIMIT:-256m}" pids_limit="${PIDS_LIMIT:-128}" suffix="${ACCEPTANCE_SUFFIX:-$(date -u +%Y%m%d%H%M%S)}" if [[ ! "$suffix" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$ ]]; then printf 'ACCEPTANCE_SUFFIX must contain only 1-64 safe name characters\n' >&2 exit 1 fi container="imagesilo-acceptance-${suffix}" password="ImageSilo-${suffix}-Acceptance-Password!" work_dir="$(realpath -m -- "$work_dir")" case "$work_dir" in /var/tmp/imagesilo-?*) ;; *) printf 'WORK_DIR must be a dedicated /var/tmp/imagesilo-* directory\n' >&2; exit 1 ;; esac mkdir -p "$work_dir" work_dir="$(realpath -- "$work_dir")" case "$work_dir" in /var/tmp/imagesilo-?*) ;; *) printf 'canonical WORK_DIR escaped /var/tmp/imagesilo-*\n' >&2; exit 1 ;; esac if find "$work_dir" -mindepth 1 -print -quit | grep -q .; then printf 'WORK_DIR must be an empty dedicated directory: %s\n' "$work_dir" >&2 exit 1 fi data_dir="${work_dir}/data" results_dir="${work_dir}/results" fixture_dir="${work_dir}/fixtures" cookie_file="${results_dir}/cookies.txt" upload_response="${results_dir}/upload.json" overview_before="${results_dir}/overview-before.json" overview_after="${results_dir}/overview-after.json" container_log="${results_dir}/container.log" result_file="${results_dir}/acceptance.json" fixture_file="${fixture_dir}/tiny.webp" case "$duration_seconds:$request_interval" in *[!0-9:]*|0:*|*:0) printf 'DURATION_SECONDS and REQUEST_INTERVAL must be positive integers\n' >&2; exit 1 ;; esac cleanup() { docker rm --force "$container" >/dev/null 2>&1 || true } trap cleanup EXIT INT TERM hash_stdin() { if command -v sha256sum >/dev/null 2>&1; then sha256sum | awk '{print $1}' else shasum -a 256 | awk '{print $1}' fi } json_field() { local file="$1" local key="$2" python3 -c 'import json, sys; print(json.load(open(sys.argv[1], encoding="utf-8"))[sys.argv[2]])' "$file" "$key" } wait_ready() { local base_url="$1" for _ in $(seq 1 120); do if curl --fail --silent "${base_url}/readyz" >/dev/null; then return 0 fi sleep 1 done docker logs "$container" || true return 1 } mkdir -p "$data_dir" "$results_dir" "$fixture_dir" printf '%s' 'UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA' | base64 --decode >"$fixture_file" docker run --rm \ --cpus "$cpu_limit" \ --memory "$memory_limit" \ --pids-limit "$pids_limit" \ --user 0:0 \ --entrypoint sh \ --volume "${data_dir}:/data" \ "$image" -c 'chown 10001:10001 /data && install -d -o 10001 -g 10001 -m 0750 /data/db /data/images /data/migrations /data/cache /data/cache/thumbnails /data/tmp' printf '%s\n' "$password" | docker run --rm --interactive \ --cpus "$cpu_limit" \ --memory "$memory_limit" \ --pids-limit "$pids_limit" \ --volume "${data_dir}:/data" \ "$image" admin create --email admin@example.com --password-stdin docker run --detach \ --name "$container" \ --cpus "$cpu_limit" \ --memory "$memory_limit" \ --pids-limit "$pids_limit" \ --publish "127.0.0.1:${port}:8080" \ --env IMAGESILO_COOKIE_SECURE=false \ --env IMAGESILO_PROCESSING_CONCURRENCY=1 \ --volume "${data_dir}:/data" \ "$image" >/dev/null base_url="http://127.0.0.1:${port}" wait_ready "$base_url" curl --fail --silent --show-error \ --cookie-jar "$cookie_file" \ --header 'Content-Type: application/json' \ --data "{\"email\":\"admin@example.com\",\"password\":\"${password}\"}" \ "${base_url}/api/v1/auth/login" >/dev/null csrf_token="$(awk '$6 == "imagesilo_csrf" { print $7 }' "$cookie_file" | tail -n 1)" test -n "$csrf_token" curl --fail --silent --show-error \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --form "file=@${fixture_file};filename=acceptance.webp;type=image/webp" \ --form 'visibility=public' \ "${base_url}/api/v1/images" >"$upload_response" image_id="$(json_field "$upload_response" id)" expected_hash="$(json_field "$upload_response" storedSha256)" test -n "$image_id" test "$expected_hash" = "$(curl --fail --silent --show-error "${base_url}/image/${image_id}" | hash_stdin)" curl --fail --silent --show-error \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --header 'Content-Type: application/json' \ --data "{\"path\":\"/legacy/acceptance.webp\",\"imageId\":\"${image_id}\",\"source\":\"remote-acceptance\"}" \ "${base_url}/api/v1/aliases" >/dev/null curl --fail --silent --show-error --cookie "$cookie_file" "${base_url}/api/v1/overview" >"$overview_before" goroutines_before="$(json_field "$overview_before" goroutines)" fds_before="$(docker exec "$container" sh -c 'find /proc/1/fd -mindepth 1 -maxdepth 1 | wc -l')" started_at="$(date +%s)" iterations=0 while test "$(( $(date +%s) - started_at ))" -lt "$duration_seconds"; do curl --fail --silent --show-error "${base_url}/healthz" >/dev/null curl --fail --silent --show-error "${base_url}/readyz" >/dev/null test "$expected_hash" = "$(curl --fail --silent --show-error "${base_url}/image/${image_id}" | hash_stdin)" test "$expected_hash" = "$(curl --fail --silent --show-error "${base_url}/legacy/acceptance.webp" | hash_stdin)" iterations="$((iterations + 1))" sleep "$request_interval" done curl --fail --silent --show-error --cookie "$cookie_file" "${base_url}/api/v1/overview" >"$overview_after" goroutines_after="$(json_field "$overview_after" goroutines)" fds_after="$(docker exec "$container" sh -c 'find /proc/1/fd -mindepth 1 -maxdepth 1 | wc -l')" temporary_files="$(docker exec "$container" sh -c 'find /data/tmp -type f | wc -l')" memory_current="$(docker exec "$container" sh -c 'cat /sys/fs/cgroup/memory.current')" memory_peak="$(docker exec "$container" sh -c 'cat /sys/fs/cgroup/memory.peak')" memory_anon="$(docker exec "$container" sh -c "awk '\$1 == \"anon\" { print \$2 }' /sys/fs/cgroup/memory.stat")" memory_file="$(docker exec "$container" sh -c "awk '\$1 == \"file\" { print \$2 }' /sys/fs/cgroup/memory.stat")" test "$goroutines_after" -le "$((goroutines_before + 4))" test "$fds_after" -le "$((fds_before + 8))" test "$temporary_files" = "0" docker stop --timeout 15 "$container" >/dev/null test "$(docker inspect --format '{{.State.ExitCode}}' "$container")" = "0" docker logs "$container" >"$container_log" 2>&1 grep -q 'shutdown requested' "$container_log" python3 - \ "$result_file" "$image" "$work_dir" "$duration_seconds" "$iterations" "$cpu_limit" "$memory_limit" \ "$goroutines_before" "$goroutines_after" "$fds_before" "$fds_after" \ "$memory_current" "$memory_peak" "$memory_anon" "$memory_file" <<'PY' import json import sys ( result_file, image, work_dir, duration_seconds, iterations, cpu_limit, memory_limit, goroutines_before, goroutines_after, fds_before, fds_after, memory_current, memory_peak, memory_anon, memory_file, ) = sys.argv[1:] with open(result_file, "w", encoding="utf-8") as output: json.dump({ "image": image, "workDir": work_dir, "durationSeconds": int(duration_seconds), "iterations": int(iterations), "cpuLimit": float(cpu_limit), "memoryLimit": memory_limit, "goroutinesBefore": int(goroutines_before), "goroutinesAfter": int(goroutines_after), "fileDescriptorsBefore": int(fds_before), "fileDescriptorsAfter": int(fds_after), "memoryCurrentBytes": int(memory_current), "memoryPeakBytes": int(memory_peak), "memoryAnonBytes": int(memory_anon), "memoryFileBytes": int(memory_file), }, output, ensure_ascii=False, indent=2) output.write("\n") PY printf 'ImageSilo limited remote acceptance passed: result=%s data=%s\n' "$result_file" "$data_dir"