#!/usr/bin/env bash
set -euo pipefail
base_url="https://sandbox.xapi.to"
read -rsp "XAPI production key: " XAPI_KEY
printf '\n'
instance_id=""
cleanup() {
if [[ -n "$instance_id" ]]; then
curl -fsS -X POST "$base_url/v1/sandboxes/$instance_id/terminate" \
-H "XAPI-Key: $XAPI_KEY" \
-H "Content-Type: application/json" \
-d "{\"idempotencyKey\":\"docs-terminate-$instance_id\"}" >/dev/null || true
fi
}
trap cleanup EXIT INT TERM
quote_json="$(curl -fsS -X POST "$base_url/v1/quotes" \
-H "XAPI-Key: $XAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"requirements": {"capabilities": ["exec", "files"]},
"maxEstimatedHourlyUsd": "0.20"
}')"
quote_id="$(jq -er '.quoteId' <<<"$quote_json")"
create_key="docs-create-$(date +%s)-$RANDOM"
create_json="$(curl -fsS -X POST "$base_url/v1/sandboxes" \
-H "XAPI-Key: $XAPI_KEY" \
-H "Content-Type: application/json" \
-d "{
\"selection\": {\"quoteId\": \"$quote_id\"},
\"metadata\": {\"client\": \"docs-curl\", \"purpose\": \"sandbox-api-test\"},
\"idempotencyKey\": \"$create_key\",
\"policy\": {\"resumeOnAccess\": false}
}")"
instance_id="$(jq -er '.id' <<<"$create_json")"
printf 'instance=%s\n' "$instance_id"
for _ in $(seq 1 90); do
detail="$(curl -fsS "$base_url/v1/sandboxes/$instance_id" \
-H "XAPI-Key: $XAPI_KEY")"
state="$(jq -r '.observedState' <<<"$detail")"
[[ "$state" == "RUNNING" ]] && break
[[ "$state" == "FAILED" || "$state" == "TERMINATED" ]] && {
jq . <<<"$detail"; exit 1;
}
sleep 2
done
[[ "${state:-}" == "RUNNING" ]] || { echo "RUNNING timeout" >&2; exit 1; }
exec_json="$(curl -fsS -X POST "$base_url/v1/sandboxes/$instance_id/commands" \
-H "XAPI-Key: $XAPI_KEY" \
-H "Content-Type: application/json" \
-d '{"command":"python3 -c \"print(6 * 7)\"","timeoutSeconds":60}')"
jq -e '.exitCode == 0 and (.stdout | contains("42"))' <<<"$exec_json" >/dev/null
cleanup
trap - EXIT INT TERM
for _ in $(seq 1 90); do
final_json="$(curl -fsS "$base_url/v1/sandboxes/$instance_id" \
-H "XAPI-Key: $XAPI_KEY")"
final_state="$(jq -r '.observedState' <<<"$final_json")"
[[ "$final_state" == "TERMINATED" || "$final_state" == "FAILED" ]] && break
sleep 2
done
jq '{id,observedState,totalCost,auditCounts}' <<<"$final_json"
[[ "$final_state" == "TERMINATED" || "$final_state" == "FAILED" ]]
# 终态之后再读取审计;此前的用量段/账期仍可能是开放状态,不能作为结算证据。
for _ in $(seq 1 30); do
operations="$(curl -fsS "$base_url/v1/sandboxes/$instance_id/audit?kind=operations&page=1&pageSize=100" \
-H "XAPI-Key: $XAPI_KEY")"
events="$(curl -fsS "$base_url/v1/sandboxes/$instance_id/audit?kind=events&page=1&pageSize=100" \
-H "XAPI-Key: $XAPI_KEY")"
usage="$(curl -fsS "$base_url/v1/sandboxes/$instance_id/audit?kind=usageSegments&page=1&pageSize=100" \
-H "XAPI-Key: $XAPI_KEY")"
billing="$(curl -fsS "$base_url/v1/sandboxes/$instance_id/audit?kind=billingPeriods&page=1&pageSize=100" \
-H "XAPI-Key: $XAPI_KEY")"
if jq -e 'any(.items[]; .type == "TERMINATE" and .status == "SUCCEEDED")' <<<"$operations" >/dev/null \
&& jq -e 'any(.items[]; .currentState == "TERMINATED" or .currentState == "FAILED")' <<<"$events" >/dev/null \
&& jq -e '(.total > 0) and all(.items[]; .endsAt != null and .status != "OPEN")' <<<"$usage" >/dev/null \
&& jq -e '(.total > 0) and all(.items[]; .endedAt != null and .status != "RESERVED")' <<<"$billing" >/dev/null; then
break
fi
sleep 2
done
jq -e 'any(.items[]; .type == "TERMINATE" and .status == "SUCCEEDED")' <<<"$operations" >/dev/null
jq -e 'any(.items[]; .currentState == "TERMINATED" or .currentState == "FAILED")' <<<"$events" >/dev/null
jq -e '(.total > 0) and all(.items[]; .endsAt != null and .status != "OPEN")' <<<"$usage" >/dev/null
jq -e '(.total > 0) and all(.items[]; .endedAt != null and .status != "RESERVED")' <<<"$billing" >/dev/null
active_json="$(curl -fsS "$base_url/v1/sandbox-history?state=ACTIVE&page=1&pageSize=100" \
-H "XAPI-Key: $XAPI_KEY")"
jq -e --arg id "$instance_id" 'all(.items[]; .id != $id)' <<<"$active_json" >/dev/null
printf 'audit and cleanup verified for %s; totalCost=%s\n' \
"$instance_id" "$(jq -r '.totalCost' <<<"$final_json")"