Files
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

561 lines
17 KiB
Bash
Executable File

#!/bin/bash
# Copyright 2025 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
color() {
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
tput setaf "$1"
fi
}
reset_color() {
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
tput sgr0
fi
}
STEP_COLOR=6 # cyan
INFO_COLOR=2 # green
WARN_COLOR=3 # yellow
ERR_COLOR=1 # red
step() {
printf "\n%s==== %s ====%s\n" "$(color ${STEP_COLOR})" "$1" "$(reset_color)"
}
info() {
printf "%s%s%s\n" "$(color ${INFO_COLOR})" "$1" "$(reset_color)"
}
warn() {
printf "%s%s%s\n" "$(color ${WARN_COLOR})" "$1" "$(reset_color)" >&2
}
error() {
printf "%s%s%s\n" "$(color ${ERR_COLOR})" "$1" "$(reset_color)" >&2
}
BASE_URL="${BASE_URL:-http://localhost:32888}"
BASE_API_URL="${BASE_URL}/v1"
curl_json() {
curl -sfSL "$@"
}
curl_json_status() {
# Returns body + trailing status code line to allow non-2xx handling.
curl -sSL -w "\n%{http_code}" "$@"
}
wait_for_running() {
local sandbox_id="${1:-${SANDBOX_ID}}"
local deadline=$((SECONDS + 30))
while true; do
local result
result=$(curl -sSL -w "\n%{http_code}" \
"${BASE_API_URL}/sandboxes/${sandbox_id}" \
| python3 -c '
import json, sys
raw = sys.stdin.read()
lines = raw.rsplit("\n", 1)
http_code = lines[-1].strip() if len(lines) > 1 else "000"
body_text = lines[0] if len(lines) > 1 else ""
if http_code == "404":
print("ERROR:not found (404) — may have failed during provisioning")
elif http_code != "200":
print(f"RETRY:HTTP {http_code}")
elif not body_text.strip():
print("RETRY:empty body")
else:
try:
data = json.loads(body_text)
state = data.get("status", {}).get("state", "")
print(f"STATE:{state}")
print(body_text)
except json.JSONDecodeError as exc:
print(f"RETRY:invalid JSON: {exc}")
') || true
local tag="${result%%:*}"
local detail="${result#*:}"
case "${tag}" in
ERROR)
error "Sandbox ${sandbox_id}: ${detail}"
return 1
;;
RETRY)
warn "GET sandbox ${sandbox_id}: ${detail}, retrying..."
if (( SECONDS >= deadline )); then
error "Sandbox ${sandbox_id} did not reach Running within 30s."
return 1
fi
sleep 1
continue
;;
STATE)
local state="${detail%%$'\n'*}"
local body="${detail#*$'\n'}"
if [[ "${state}" == "Running" ]]; then
printf '%s' "${body}"
return 0
fi
if [[ "${state}" == "Failed" || "${state}" == "Terminated" ]]; then
error "Sandbox ${sandbox_id} entered terminal state '${state}'."
return 1
fi
if (( SECONDS >= deadline )); then
error "Sandbox ${sandbox_id} did not reach Running within 30s (last: ${state})."
return 1
fi
sleep 1
;;
*)
warn "GET sandbox ${sandbox_id}: unexpected output, retrying..."
if (( SECONDS >= deadline )); then
error "Sandbox ${sandbox_id} did not reach Running within 30s."
return 1
fi
sleep 1
;;
esac
done
}
wait_for_expired() {
local sandbox_id=$1
local deadline=$((SECONDS + 90))
while true; do
local resp body status
resp=$(curl_json_status "${BASE_API_URL}/sandboxes/${sandbox_id}")
status="${resp##*$'\n'}"
body="${resp%$'\n'*}"
if [[ "${status}" == "404" ]]; then
info "Sandbox ${sandbox_id} expired as expected."
return 0
fi
if (( SECONDS >= deadline )); then
error "Sandbox ${sandbox_id} did not expire within expected window (last status ${status})."
echo "${body}"
return 1
fi
sleep 2
done
}
wait_for_sidecar_gone() {
local sandbox_id=$1
local deadline=$((SECONDS + 20))
while true; do
if ! docker ps -a --filter "label=opensandbox.io/egress-sidecar-for=${sandbox_id}" -q | grep -q .; then
info "No sidecar remaining for sandbox ${sandbox_id}"
return 0
fi
if (( SECONDS >= deadline )); then
error "Sidecar for sandbox ${sandbox_id} still present after timeout"
docker ps -a --filter "label=opensandbox.io/egress-sidecar-for=${sandbox_id}"
return 1
fi
sleep 2
done
}
docker pull ubuntu:latest
create_payload='{
"image": { "uri": "ubuntu" },
"env": { "HELLO": "WORLD" },
"metadata": { "hello": "world" },
"entrypoint": ["tail", "-f", "/dev/null"],
"resourceLimits": { "cpu": "500m", "memory": "512Mi" },
"timeout": 60
}'
step "Create sandbox (60s TTL)"
create_resp=$(curl_json \
-H 'Content-Type: application/json' \
-d "${create_payload}" \
"${BASE_API_URL}/sandboxes")
SANDBOX_ID=$(python3 - <<'PY' "${create_resp}"
import json,sys
data=json.loads(sys.argv[1])
sid=str(data.get("id","")).strip()
if not sid:
raise SystemExit("Failed to parse sandbox id from response")
print(sid,end="")
PY
)
echo "Sandbox created: id=${SANDBOX_ID}"
step "Wait for sandbox to reach Running"
get_resp=$(wait_for_running)
state=$(python3 - <<'PY' "${get_resp}"
import json,sys
body=json.loads(sys.argv[1])
print(body.get("status",{}).get("state"))
PY
)
echo "Sandbox state: ${state}"
python3 - <<'PY' "${get_resp}" "${SANDBOX_ID}"
import json,sys
body=json.loads(sys.argv[1])
expected=sys.argv[2]
assert str(body.get("id"))==expected, "Sandbox ID mismatch in GET response"
assert body.get("status",{}).get("state") in {"Pending","Running","Unknown","Paused","Terminated","Failed"}, "Unexpected state"
PY
step "List sandboxes (metadata filter)"
list_resp=$(curl_json \
-G \
--data-urlencode "metadata=hello=world" \
--data-urlencode "page=1" \
--data-urlencode "pageSize=10" \
"${BASE_API_URL}/sandboxes")
python3 - <<'PY' "${list_resp}" "${SANDBOX_ID}"
import json,sys
body=json.loads(sys.argv[1])
sid=sys.argv[2]
ids=[item.get("id") for item in body.get("items",[])]
assert sid in ids, "Sandbox ID not found in list response"
assert body.get("pagination",{}).get("page") == 1, "Unexpected pagination page"
PY
echo "List check passed (found sandbox, pagination ok)"
step "Renew sandbox expiration (+10m)"
new_expiration=$(python3 - <<'PY'
from datetime import datetime, timedelta, timezone
print((datetime.now(timezone.utc) + timedelta(minutes=10)).isoformat())
PY
)
renew_payload=$(cat <<JSON
{
"expiresAt": "${new_expiration}"
}
JSON
)
renew_resp=$(curl_json \
-X POST \
-H 'Content-Type: application/json' \
-d "${renew_payload}" \
"${BASE_API_URL}/sandboxes/${SANDBOX_ID}/renew-expiration")
renewed=$(python3 - <<'PY' "${renew_resp}"
import json,sys
body=json.loads(sys.argv[1])
print(body.get("expiresAt"))
PY
)
echo "Expiration renewed to: ${renewed}"
curl_patch() {
# Like curl_json but dumps response body on HTTP errors for debugging.
local result headers http_code
result=$(curl -sSL -D /dev/stderr -w "\n%{http_code}" "$@") || true
http_code="${result##*$'\n'}"
if [[ "${http_code}" -ge 400 ]]; then
echo ""
echo "PATCH HTTP ${http_code} — response body:" >&2
echo "${result%$'\n'*}" >&2
return 1
fi
printf '%s' "${result%$'\n'*}"
}
step "Patch sandbox metadata — add key"
patch_resp=$(curl_patch \
-X PATCH \
-H 'Content-Type: application/json' \
-d '{"team": "platform", "version": "2.0"}' \
"${BASE_API_URL}/sandboxes/${SANDBOX_ID}/metadata")
python3 - <<'PY' "${patch_resp}" "${SANDBOX_ID}"
import json,sys
body=json.loads(sys.argv[1])
sid=sys.argv[2]
assert str(body.get("id"))==sid, "Sandbox ID mismatch in PATCH response"
md=body.get("metadata") or {}
# Original key still present
assert md.get("hello")=="world", f"Expected hello=world, got {md.get('hello')}"
# New keys added
assert md.get("team")=="platform", f"Expected team=platform, got {md.get('team')}"
assert md.get("version")=="2.0", f"Expected version=2.0, got {md.get('version')}"
print("PASS: metadata after add: " + json.dumps(md))
PY
info "PATCH add keys OK"
step "Patch sandbox metadata — delete key"
patch_resp=$(curl_patch \
-X PATCH \
-H 'Content-Type: application/json' \
-d '{"version": null}' \
"${BASE_API_URL}/sandboxes/${SANDBOX_ID}/metadata")
python3 - <<'PY' "${patch_resp}" "${SANDBOX_ID}"
import json,sys
body=json.loads(sys.argv[1])
sid=sys.argv[2]
md=body.get("metadata") or {}
assert "version" not in md, f"version should be deleted, got {md.get('version')}"
assert md.get("team")=="platform", f"Expected team=platform, got {md.get('team')}"
assert md.get("hello")=="world", f"Expected hello=world, got {md.get('hello')}"
print("PASS: metadata after delete: " + json.dumps(md))
PY
info "PATCH delete key OK"
step "Patch sandbox metadata — mixed add and delete"
patch_resp=$(curl_patch \
-X PATCH \
-H 'Content-Type: application/json' \
-d '{"team": null, "env": "production"}' \
"${BASE_API_URL}/sandboxes/${SANDBOX_ID}/metadata")
python3 - <<'PY' "${patch_resp}" "${SANDBOX_ID}"
import json,sys
body=json.loads(sys.argv[1])
sid=sys.argv[2]
md=body.get("metadata") or {}
assert "team" not in md, f"team should be deleted, got {md.get('team')}"
assert md.get("env")=="production", f"Expected env=production, got {md.get('env')}"
assert md.get("hello")=="world", f"Expected hello=world, got {md.get('hello')}"
print("PASS: metadata after mixed: " + json.dumps(md))
PY
info "PATCH mixed operations OK"
step "Patch sandbox metadata — empty body noop"
patch_resp=$(curl_patch \
-X PATCH \
-H 'Content-Type: application/json' \
-d '{}' \
"${BASE_API_URL}/sandboxes/${SANDBOX_ID}/metadata")
python3 - <<'PY' "${patch_resp}" "${SANDBOX_ID}"
import json,sys
body=json.loads(sys.argv[1])
sid=sys.argv[2]
md=body.get("metadata") or {}
assert md.get("hello")=="world", f"Expected hello=world, got {md.get('hello')}"
assert md.get("env")=="production", f"Expected env=production, got {md.get('env')}"
print("PASS: empty body noop: " + json.dumps(md))
PY
info "PATCH empty body noop OK"
step "Request endpoint on port 8080"
endpoint_resp=$(curl_json "${BASE_API_URL}/sandboxes/${SANDBOX_ID}/endpoints/8080")
endpoint=$(python3 - <<'PY' "${endpoint_resp}"
import json,sys
body=json.loads(sys.argv[1])
print(body.get("endpoint"))
PY
)
echo "Endpoint: ${endpoint}"
step "Delete sandbox"
curl_json -X DELETE "${BASE_API_URL}/sandboxes/${SANDBOX_ID}"
echo "Sandbox ${SANDBOX_ID} deleted."
step "Create sandbox with networkPolicy (egress sidecar)"
egress_payload='{
"image": { "uri": "ubuntu" },
"env": {},
"metadata": { "egress": "on" },
"entrypoint": ["tail", "-f", "/dev/null"],
"resourceLimits": { "cpu": "500m", "memory": "512Mi" },
"timeout": 60,
"networkPolicy": {
"defaultAction": "deny",
"egress": [
{ "action": "allow", "target": "pypi.org" }
]
}
}'
create_resp_with_status=$(curl_json_status \
-H 'Content-Type: application/json' \
-d "${egress_payload}" \
"${BASE_API_URL}/sandboxes")
status_code="${create_resp_with_status##*$'\n'}"
create_resp_body="${create_resp_with_status%$'\n'*}"
if [[ "${status_code}" != "202" ]]; then
warn "Skip egress sidecar smoke (status ${status_code}). Body: ${create_resp_body}"
warn "Likely network_mode=host or egress.image unset."
else
SANDBOX_ID=$(python3 - <<'PY' "${create_resp_body}"
import json,sys
data=json.loads(sys.argv[1])
sid=str(data.get("id","")).strip()
if not sid:
raise SystemExit("Failed to parse sandbox id from response")
print(sid,end="")
PY
)
echo "Egress sandbox created: id=${SANDBOX_ID}"
step "Wait for egress sandbox to reach Running"
wait_for_running "${SANDBOX_ID}" >/dev/null
step "Verify egress sidecar is running"
SIDECAR_ID=$(docker ps -a --filter "label=opensandbox.io/egress-sidecar-for=${SANDBOX_ID}" -q | head -n1 || true)
if [[ -z "${SIDECAR_ID}" ]]; then
error "Expected egress sidecar for sandbox ${SANDBOX_ID}, but none found."
exit 1
fi
info "Sidecar ${SIDECAR_ID} detected for sandbox ${SANDBOX_ID}"
step "Delete egress sandbox and ensure sidecar cleanup"
curl_json -X DELETE "${BASE_API_URL}/sandboxes/${SANDBOX_ID}"
wait_for_sidecar_gone "${SANDBOX_ID}"
fi
step "Create sandbox with host volume mount"
# Prepare the host volume test directory
mkdir -p /tmp/opensandbox-e2e/host-volume-test
echo "opensandbox-e2e-marker" > /tmp/opensandbox-e2e/host-volume-test/marker.txt
chmod -R 755 /tmp/opensandbox-e2e
volume_payload='{
"image": { "uri": "ubuntu" },
"env": {},
"metadata": { "volume": "host-test" },
"entrypoint": ["tail", "-f", "/dev/null"],
"resourceLimits": { "cpu": "500m", "memory": "512Mi" },
"timeout": 60,
"volumes": [
{
"name": "test-host-vol",
"host": { "path": "/tmp/opensandbox-e2e/host-volume-test" },
"mountPath": "/mnt/host-data",
"readOnly": false
}
]
}'
volume_resp_with_status=$(curl_json_status \
-H 'Content-Type: application/json' \
-d "${volume_payload}" \
"${BASE_API_URL}/sandboxes")
volume_status="${volume_resp_with_status##*$'\n'}"
volume_body="${volume_resp_with_status%$'\n'*}"
if [[ "${volume_status}" != "202" ]]; then
warn "Skip host volume smoke (status ${volume_status}). Body: ${volume_body}"
warn "Likely host path validation or storage config issue."
else
VOLUME_SANDBOX_ID=$(python3 - <<'PY' "${volume_body}"
import json,sys
data=json.loads(sys.argv[1])
sid=str(data.get("id","")).strip()
if not sid:
raise SystemExit("Failed to parse sandbox id from response")
print(sid,end="")
PY
)
echo "Volume sandbox created: id=${VOLUME_SANDBOX_ID}"
step "Wait for volume sandbox to reach Running"
wait_for_running "${VOLUME_SANDBOX_ID}" >/dev/null
# --- Verify the bind mount is actually effective ---
# Resolve the Docker container ID from the sandbox API response.
volume_sandbox_resp=$(curl_json "${BASE_API_URL}/sandboxes/${VOLUME_SANDBOX_ID}")
container_id=$(python3 -c '
import json, sys
body = json.loads(sys.argv[1])
print(body.get("containerId", body.get("container_id", "")), end="")
' "${volume_sandbox_resp}")
# Fallback: if the API doesn't expose container_id, search by label.
if [[ -z "${container_id}" ]]; then
container_id=$(docker ps -qf "label=sandbox_id=${VOLUME_SANDBOX_ID}" | head -1)
fi
if [[ -n "${container_id}" ]]; then
step "Verify host volume bind mount content inside container"
# 1. Read the marker file written on the host
marker_content=$(docker exec "${container_id}" cat /mnt/host-data/marker.txt 2>&1) || true
if [[ "${marker_content}" == "opensandbox-e2e-marker" ]]; then
info "PASS: marker.txt content matches expected value."
else
error "FAIL: marker.txt content='${marker_content}', expected='opensandbox-e2e-marker'"
exit 1
fi
# 2. Write a file from inside the container and verify it on the host
docker exec "${container_id}" sh -c 'echo "written-from-sandbox" > /mnt/host-data/sandbox-output.txt'
host_content=$(cat /tmp/opensandbox-e2e/host-volume-test/sandbox-output.txt 2>&1) || true
if [[ "${host_content}" == "written-from-sandbox" ]]; then
info "PASS: file written inside container is visible on host."
else
error "FAIL: sandbox-output.txt on host='${host_content}', expected='written-from-sandbox'"
exit 1
fi
else
warn "Skip bind-mount verification: could not resolve container ID for sandbox ${VOLUME_SANDBOX_ID}."
fi
step "Delete volume sandbox"
curl_json -X DELETE "${BASE_API_URL}/sandboxes/${VOLUME_SANDBOX_ID}"
echo "Volume sandbox ${VOLUME_SANDBOX_ID} deleted."
fi
step "Create short-lived sandbox (60s TTL) for auto-expiration"
create_payload_short='{
"image": { "uri": "ubuntu" },
"env": {},
"metadata": { "lifecycle": "short" },
"entrypoint": ["tail", "-f", "/dev/null"],
"resourceLimits": { "cpu": "1", "memory": "2Gi" },
"timeout": 60
}'
create_resp_short=$(curl_json \
-H 'Content-Type: application/json' \
-d "${create_payload_short}" \
"${BASE_API_URL}/sandboxes")
SANDBOX_ID=$(python3 - <<'PY' "${create_resp_short}"
import json,sys
data=json.loads(sys.argv[1])
sid=str(data.get("id","")).strip()
if not sid:
raise SystemExit("Failed to parse sandbox id from response")
print(sid,end="")
PY
)
echo "Short-lived sandbox created: id=${SANDBOX_ID}"
step "Wait for short-lived sandbox to reach Running"
get_resp_short=$(wait_for_running "${SANDBOX_ID}")
state_short=$(python3 - <<'PY' "${get_resp_short}"
import json,sys
body=json.loads(sys.argv[1])
print(body.get("status",{}).get("state"))
PY
)
echo "Sandbox state: ${state_short}"
step "Wait for sandbox ${SANDBOX_ID} to auto-expire (expect 404)"
wait_for_expired "${SANDBOX_ID}"
step "server Lifecycle API smoke test completed successfully"