amduat-api/scripts/test_fed_ingest.sh
2026-02-08 08:13:10 +01:00

393 lines
10 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HTTP_HELPER="${ROOT_DIR}/build/amduatd_http_unix"
USE_HTTP_HELPER=0
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
if ! command -v grep >/dev/null 2>&1; then
echo "skip: grep not found" >&2
exit 77
fi
if ! command -v awk >/dev/null 2>&1; then
echo "skip: awk not found" >&2
exit 77
fi
if command -v curl >/dev/null 2>&1; then
if curl --help 2>/dev/null | grep -q -- '--unix-socket'; then
USE_HTTP_HELPER=0
else
USE_HTTP_HELPER=1
fi
else
USE_HTTP_HELPER=1
fi
if [[ "${USE_HTTP_HELPER}" -eq 1 && ! -x "${HTTP_HELPER}" ]]; then
echo "skip: curl lacks --unix-socket support and helper missing" >&2
exit 77
fi
AMDUATD_BIN="${AMDUATD_BIN:-}"
if [[ -z "${AMDUATD_BIN}" ]]; then
for cand in \
"${ROOT_DIR}/build/amduatd" \
"${ROOT_DIR}/build-asan/amduatd"; do
if [[ -x "${cand}" ]]; then
AMDUATD_BIN="${cand}"
break
fi
done
fi
ASL_BIN="${ASL_BIN:-}"
if [[ -z "${ASL_BIN}" ]]; then
for cand in \
"${ROOT_DIR}/build/vendor/amduat/amduat-asl" \
"${ROOT_DIR}/vendor/amduat/build/amduat-asl"; do
if [[ -x "${cand}" ]]; then
ASL_BIN="${cand}"
break
fi
done
fi
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-fed-ingest-XXXXXX)"
tmp_keep="${AMDUATD_FED_INGEST_KEEP_TMP:-0}"
last_log="${TMPDIR}/amduatd-fed-ingest.last.log"
root_store="${tmp_root}/store"
root_ref="${tmp_root}/ref"
sock="${tmp_root}/amduatd.sock"
log="${tmp_root}/amduatd.log"
space_a="alpha"
space_b="beta"
show_log() {
if [[ -n "${log}" && -f "${log}" ]]; then
echo "daemon log: ${log}" >&2
cat "${log}" >&2
else
echo "daemon log missing: ${log}" >&2
fi
if [[ -f "${last_log}" ]]; then
echo "last log copy: ${last_log}" >&2
fi
}
die() {
echo "$1" >&2
show_log
exit 1
}
cleanup() {
if [[ -n "${pid:-}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
fi
if [[ -f "${log}" ]]; then
cp -f "${log}" "${last_log}" 2>/dev/null || true
fi
if [[ "${tmp_keep}" -eq 0 ]]; then
rm -rf "${tmp_root}"
else
echo "kept tmpdir: ${tmp_root}" >&2
fi
}
trap cleanup EXIT
mkdir -p "${root_store}" "${root_ref}"
"${ASL_BIN}" index init --root "${root_store}"
"${ASL_BIN}" index init --root "${root_ref}"
"${AMDUATD_BIN}" --root "${root_store}" --sock "${sock}" \
--store-backend index --space "${space_a}" \
--fed-enable --fed-transport stub \
>"${log}" 2>&1 &
pid=$!
http_get() {
local sock_path="$1"
local path="$2"
shift 2
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock_path}" --method GET --path "${path}" "$@"
else
curl --silent --show-error --fail \
--unix-socket "${sock_path}" \
"$@" \
"http://localhost${path}"
fi
}
http_get_allow() {
local sock_path="$1"
local path="$2"
shift 2
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock_path}" --method GET --path "${path}" \
--allow-status "$@"
else
curl --silent --show-error \
--unix-socket "${sock_path}" \
"$@" \
"http://localhost${path}"
fi
}
http_post() {
local sock_path="$1"
local path="$2"
local data="$3"
shift 3
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock_path}" --method POST --path "${path}" \
--data "${data}" \
"$@"
else
curl --silent --show-error --fail \
--unix-socket "${sock_path}" \
"$@" \
--data-binary "${data}" \
"http://localhost${path}"
fi
}
http_post_allow() {
local sock_path="$1"
local path="$2"
local data="$3"
shift 3
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock_path}" --method POST --path "${path}" \
--allow-status --data "${data}" \
"$@"
else
curl --silent --show-error \
--unix-socket "${sock_path}" \
"$@" \
--data-binary "${data}" \
"http://localhost${path}"
fi
}
http_status() {
local sock_path="$1"
local method="$2"
local path="$3"
shift 3
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
echo ""
return 0
fi
curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
--unix-socket "${sock_path}" \
-X "${method}" \
"$@" \
"http://localhost${path}"
}
wait_for_ready() {
local sock_path="$1"
local pid_val="$2"
local log_path="$3"
local i
for i in $(seq 1 100); do
if ! kill -0 "${pid_val}" >/dev/null 2>&1; then
if [[ -f "${log_path}" ]] && grep -q "bind: Operation not permitted" "${log_path}"; then
echo "skip: bind not permitted for unix socket" >&2
exit 77
fi
if [[ -f "${log_path}" ]]; then
cat "${log_path}" >&2
fi
return 1
fi
if [[ -S "${sock_path}" ]]; then
if http_get "${sock_path}" "/v1/meta" >/dev/null 2>&1; then
return 0
fi
fi
sleep 0.1
done
return 1
}
if ! wait_for_ready "${sock}" "${pid}" "${log}"; then
die "daemon not ready"
fi
payload="fed-ingest"
ref="$(
printf '%s' "${payload}" | "${ASL_BIN}" put --root "${root_ref}" \
--input - --input-format raw --ref-format hex \
| tail -n 1 | tr -d '\r\n'
)"
ingest_resp="$(
http_post "${sock}" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \
"${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_a}"
)" || {
die "ingest artifact failed"
}
status="$(
printf '%s' "${ingest_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}'
)"
applied="$(
printf '%s' "${ingest_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"applied":[^,}]+/) {print substr($0, RSTART+10, RLENGTH-10)}'
)"
if [[ "${status}" != "ok" || "${applied}" != "true" ]]; then
die "unexpected ingest response: ${ingest_resp}"
fi
fetched="$(http_get "${sock}" "/v1/artifacts/${ref}")"
if [[ "${fetched}" != "${payload}" ]]; then
die "artifact fetch mismatch"
fi
ingest_again="$(
http_post "${sock}" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \
"${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_a}"
)"
status="$(
printf '%s' "${ingest_again}" \
| tr -d '\r\n' \
| awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}'
)"
applied="$(
printf '%s' "${ingest_again}" \
| tr -d '\r\n' \
| awk 'match($0, /"applied":[^,}]+/) {print substr($0, RSTART+10, RLENGTH-10)}'
)"
if [[ "${status}" != "already_present" && "${applied}" != "false" ]]; then
die "unexpected re-ingest response: ${ingest_again}"
fi
tombstone_resp="$(
http_post "${sock}" "/v1/fed/ingest" \
"{\"record_type\":\"tombstone\",\"ref\":\"${ref}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_a}"
)"
status="$(
printf '%s' "${tombstone_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}'
)"
if [[ "${status}" != "ok" ]]; then
die "unexpected tombstone response: ${tombstone_resp}"
fi
http_get_allow "${sock}" "/v1/artifacts/${ref}" >/dev/null 2>&1 || true
tombstone_again="$(
http_post "${sock}" "/v1/fed/ingest" \
"{\"record_type\":\"tombstone\",\"ref\":\"${ref}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_a}"
)"
status="$(
printf '%s' "${tombstone_again}" \
| tr -d '\r\n' \
| awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}'
)"
if [[ "${status}" != "ok" && "${status}" != "already_present" ]]; then
die "unexpected tombstone repeat response: ${tombstone_again}"
fi
cap_resp="$(
http_post "${sock}" "/v1/capabilities" \
"{\"kind\":\"pointer_name\",\"target\":{\"name\":\"space/${space_a}/fed/records\"},\"expiry_seconds\":3600}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_a}"
)"
cap_token="$(
printf '%s' "${cap_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"token":"[^"]+"/) {print substr($0, RSTART+9, RLENGTH-10)}'
)"
if [[ -z "${cap_token}" ]]; then
die "failed to mint capability: ${cap_resp}"
fi
wrong_space_resp="$(
http_post_allow "${sock}" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \
"${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_b}" \
--header "X-Amduat-Capability: ${cap_token}"
)"
status="$(
printf '%s' "${wrong_space_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}'
)"
if [[ "${status}" != "invalid" ]]; then
die "unexpected wrong-space response: ${wrong_space_resp}"
fi
if [[ "${USE_HTTP_HELPER}" -eq 0 ]]; then
code="$(http_status "${sock}" "POST" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_b}" \
--header "X-Amduat-Capability: ${cap_token}")"
if [[ "${code}" != "403" ]]; then
die "expected 403 for wrong-space capability, got ${code}"
fi
fi
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" >/dev/null 2>&1 || true
pid=""
rm -f "${sock}"
"${AMDUATD_BIN}" --root "${root_store}" --sock "${sock}" \
--store-backend index --space "${space_a}" \
--fed-transport stub \
>"${log}" 2>&1 &
pid=$!
if ! wait_for_ready "${sock}" "${pid}" "${log}"; then
die "daemon (fed disabled) not ready"
fi
disabled_resp="$(
http_post_allow "${sock}" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \
"${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_a}"
)"
status="$(
printf '%s' "${disabled_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}'
)"
if [[ "${status}" != "error" ]]; then
die "unexpected disabled response: ${disabled_resp}"
fi
if [[ "${USE_HTTP_HELPER}" -eq 0 ]]; then
code="$(http_status "${sock}" "POST" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_a}")"
if [[ "${code}" != "503" ]]; then
die "expected 503 for federation disabled, got ${code}"
fi
fi
echo "ok"