test: add first deployment console e2e
This commit is contained in:
223
scripts/test-console-e2e.sh
Executable file
223
scripts/test-console-e2e.sh
Executable file
@@ -0,0 +1,223 @@
|
||||
#!/bin/sh
|
||||
# 在临时 Docker 网络和一次性 MySQL 中验证首次初始化、控制台登录、任务执行、重试和定时任务。
|
||||
set -eu
|
||||
|
||||
E2E_DOCKER=${DOCKER:-docker}
|
||||
E2E_GO=${GO:-go}
|
||||
E2E_IMAGE=${ORBIT_E2E_IMAGE:-orbit:e2e}
|
||||
E2E_SUFFIX=$$
|
||||
E2E_MYSQL="orbit-e2e-mysql-$E2E_SUFFIX"
|
||||
E2E_ORBIT="orbit-e2e-server-$E2E_SUFFIX"
|
||||
E2E_DIR=$(mktemp -d "${TMPDIR:-/tmp}/orbit-console-e2e.XXXXXX")
|
||||
E2E_PASSWORD_FILE="$E2E_DIR/console-password"
|
||||
E2E_CREDENTIALS_FILE="$E2E_DIR/credentials.json"
|
||||
|
||||
cleanup() {
|
||||
"$E2E_DOCKER" stop "$E2E_ORBIT" "$E2E_MYSQL" >/dev/null 2>&1 || true
|
||||
"$E2E_DOCKER" container rm "$E2E_ORBIT" "$E2E_MYSQL" >/dev/null 2>&1 || true
|
||||
for e2e_artifact in "$E2E_DIR"/*; do
|
||||
if [ -f "$e2e_artifact" ]; then
|
||||
unlink "$e2e_artifact"
|
||||
fi
|
||||
done
|
||||
rmdir "$E2E_DIR" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
umask 077
|
||||
E2E_DB_PASSWORD=$(openssl rand -hex 24)
|
||||
E2E_CONSOLE_PASSWORD=$(openssl rand -hex 24)
|
||||
E2E_APPLICATION="console-e2e-$E2E_SUFFIX"
|
||||
E2E_ADMIN="orbit-admin-$E2E_SUFFIX"
|
||||
printf '%s\n' "$E2E_CONSOLE_PASSWORD" > "$E2E_PASSWORD_FILE"
|
||||
|
||||
printf '%s\n' "[1/8] build image"
|
||||
"$E2E_DOCKER" build --tag "$E2E_IMAGE" . >/dev/null
|
||||
"$E2E_DOCKER" run -d --name "$E2E_MYSQL" --network bridge \
|
||||
--env MYSQL_ROOT_PASSWORD="$E2E_DB_PASSWORD" \
|
||||
--env MYSQL_DATABASE=orbit_test mysql:8.4 >/dev/null
|
||||
|
||||
E2E_READY=0
|
||||
for e2e_attempt in $(seq 1 60); do
|
||||
if "$E2E_DOCKER" exec "$E2E_MYSQL" sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --protocol=tcp -e "SELECT 1" >/dev/null 2>&1'; then
|
||||
E2E_READY=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$E2E_READY" -ne 1 ]; then
|
||||
printf '%s\n' 'temporary MySQL failed to become ready' >&2
|
||||
"$E2E_DOCKER" logs --tail 40 "$E2E_MYSQL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
E2E_DSN="root:$E2E_DB_PASSWORD@tcp(mysql:3306)/orbit_test"
|
||||
printf '%s\n' "[2/8] migrate empty database"
|
||||
"$E2E_DOCKER" run --rm --network bridge --link "$E2E_MYSQL:mysql" \
|
||||
--env ORBIT_MYSQL_DSN="$E2E_DSN" \
|
||||
--entrypoint /usr/local/bin/orbit-migrate "$E2E_IMAGE" up >/dev/null
|
||||
|
||||
printf '%s\n' "[3/8] bootstrap resources and console administrator"
|
||||
"$E2E_DOCKER" run --rm -i --network bridge --link "$E2E_MYSQL:mysql" \
|
||||
--env ORBIT_MYSQL_DSN="$E2E_DSN" \
|
||||
--volume "$E2E_DIR:/run/orbit" \
|
||||
--entrypoint /usr/local/bin/orbit-admin "$E2E_IMAGE" bootstrap \
|
||||
--application "$E2E_APPLICATION" \
|
||||
--namespace default \
|
||||
--environment test \
|
||||
--queue files \
|
||||
--worker-access \
|
||||
--schedule-access \
|
||||
--console-username "$E2E_ADMIN" \
|
||||
--console-display-name 'Orbit E2E 管理员' \
|
||||
--console-password-stdin \
|
||||
--output /run/orbit/credentials.json < "$E2E_PASSWORD_FILE" >/dev/null
|
||||
E2E_QUEUE=$(python3 - "$E2E_CREDENTIALS_FILE" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
value = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
queue = value.get("queue_id", "")
|
||||
if not queue:
|
||||
raise SystemExit("bootstrap output has no queue_id")
|
||||
print(queue)
|
||||
PY
|
||||
)
|
||||
|
||||
printf '%s\n' "[4/8] start Orbit container"
|
||||
"$E2E_DOCKER" run -d --name "$E2E_ORBIT" --network bridge --link "$E2E_MYSQL:mysql" \
|
||||
--env ORBIT_MYSQL_DSN="$E2E_DSN" \
|
||||
--env ORBIT_HTTP_ADDR=0.0.0.0:8080 \
|
||||
--env ORBIT_CONSOLE_QUEUE_ID="$E2E_QUEUE" \
|
||||
--publish 127.0.0.1::8080 "$E2E_IMAGE" >/dev/null
|
||||
E2E_PORT=
|
||||
for e2e_attempt in $(seq 1 30); do
|
||||
E2E_PORT=$("$E2E_DOCKER" port "$E2E_ORBIT" 8080/tcp 2>/dev/null | sed -n 's/.*:\([0-9][0-9]*\)$/\1/p' | head -n 1)
|
||||
if [ -n "$E2E_PORT" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ -z "$E2E_PORT" ]; then
|
||||
printf '%s\n' 'Orbit container did not publish HTTP port' >&2
|
||||
"$E2E_DOCKER" logs --tail 60 "$E2E_ORBIT" >&2
|
||||
exit 1
|
||||
fi
|
||||
E2E_URL="http://127.0.0.1:$E2E_PORT"
|
||||
E2E_READY=0
|
||||
for e2e_attempt in $(seq 1 30); do
|
||||
if curl -fsS "$E2E_URL/health/ready" >/dev/null 2>&1; then
|
||||
E2E_READY=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$E2E_READY" -ne 1 ]; then
|
||||
printf '%s\n' 'Orbit readiness check failed' >&2
|
||||
"$E2E_DOCKER" logs --tail 60 "$E2E_ORBIT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "[5/8] console login and protected-page check"
|
||||
ORBIT_E2E_URL="$E2E_URL" ORBIT_E2E_QUEUE="$E2E_QUEUE" ORBIT_E2E_USER="$E2E_ADMIN" \
|
||||
ORBIT_E2E_PASSWORD_FILE="$E2E_PASSWORD_FILE" python3 - <<'PY'
|
||||
import http.cookiejar
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
base = os.environ["ORBIT_E2E_URL"]
|
||||
queue = os.environ["ORBIT_E2E_QUEUE"]
|
||||
username = os.environ["ORBIT_E2E_USER"]
|
||||
password = pathlib.Path(os.environ["ORBIT_E2E_PASSWORD_FILE"]).read_text(encoding="utf-8").strip()
|
||||
jar = http.cookiejar.CookieJar()
|
||||
client = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
|
||||
|
||||
def request(path, method="GET", body=None):
|
||||
request_body = None if body is None else json.dumps(body).encode("utf-8")
|
||||
request = urllib.request.Request(base + path, method=method, data=request_body)
|
||||
request.add_header("Accept", "application/json")
|
||||
if body is not None:
|
||||
request.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with client.open(request, timeout=8) as response:
|
||||
return response.status, response.read()
|
||||
except urllib.error.HTTPError as error:
|
||||
return error.code, error.read()
|
||||
|
||||
status, _ = request("/console/auth/session")
|
||||
assert status == 200, status
|
||||
status, _ = request("/console/auth/login", "POST", {"username": username, "password": password})
|
||||
assert status == 200, status
|
||||
status, body = request("/console/auth/session")
|
||||
assert status == 200 and json.loads(body).get("authenticated") is True
|
||||
status, body = request("/console/")
|
||||
assert status == 200 and "运行统计".encode("utf-8") in body, status
|
||||
status, body = request("/console/config")
|
||||
assert status == 200 and json.loads(body).get("default_queue_id") == queue
|
||||
status, _ = request("/v1/tasks?" + urllib.parse.urlencode({"queue_id": queue, "limit": 20}))
|
||||
assert status == 200, status
|
||||
print("console_login=ok protected_page=ok")
|
||||
PY
|
||||
|
||||
printf '%s\n' "[6/8] WebSocket Worker task execution"
|
||||
(cd sdk/go && ORBIT_URL="$E2E_URL" ORBIT_CREDENTIALS_FILE="$E2E_CREDENTIALS_FILE" \
|
||||
"$E2E_GO" run ./examples/worker) | tee "$E2E_DIR/worker.log"
|
||||
grep -q 'worker_drained=true started=3 confirmed=3 running=0' "$E2E_DIR/worker.log"
|
||||
|
||||
printf '%s\n' "[7/8] retry and schedule execution"
|
||||
(cd sdk/go && ORBIT_URL="$E2E_URL" ORBIT_CREDENTIALS_FILE="$E2E_CREDENTIALS_FILE" \
|
||||
"$E2E_GO" run ./examples/retry) | tee "$E2E_DIR/retry.log"
|
||||
grep -q 'attempts=2' "$E2E_DIR/retry.log"
|
||||
(cd sdk/go && ORBIT_URL="$E2E_URL" ORBIT_CREDENTIALS_FILE="$E2E_CREDENTIALS_FILE" \
|
||||
"$E2E_GO" run ./examples/schedules) | tee "$E2E_DIR/schedule.log"
|
||||
grep -q 'source_kind=SCHEDULE' "$E2E_DIR/schedule.log"
|
||||
|
||||
printf '%s\n' "[8/8] final console data and logout check"
|
||||
ORBIT_E2E_URL="$E2E_URL" ORBIT_E2E_QUEUE="$E2E_QUEUE" ORBIT_E2E_PASSWORD_FILE="$E2E_PASSWORD_FILE" \
|
||||
ORBIT_E2E_USER="$E2E_ADMIN" python3 - <<'PY'
|
||||
import http.cookiejar
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
base = os.environ["ORBIT_E2E_URL"]
|
||||
queue = os.environ["ORBIT_E2E_QUEUE"]
|
||||
username = os.environ["ORBIT_E2E_USER"]
|
||||
password = pathlib.Path(os.environ["ORBIT_E2E_PASSWORD_FILE"]).read_text(encoding="utf-8").strip()
|
||||
jar = http.cookiejar.CookieJar()
|
||||
client = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
|
||||
|
||||
def request(path, method="GET", body=None):
|
||||
data = None if body is None else json.dumps(body).encode("utf-8")
|
||||
req = urllib.request.Request(base + path, method=method, data=data)
|
||||
req.add_header("Accept", "application/json")
|
||||
if body is not None:
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with client.open(req, timeout=8) as response:
|
||||
return response.status, response.read()
|
||||
except urllib.error.HTTPError as error:
|
||||
return error.code, error.read()
|
||||
|
||||
status, _ = request("/console/auth/login", "POST", {"username": username, "password": password})
|
||||
assert status == 200, status
|
||||
status, body = request("/v1/tasks?" + urllib.parse.urlencode({"queue_id": queue, "limit": 100}))
|
||||
assert status == 200, status
|
||||
items = json.loads(body).get("data", {}).get("items", [])
|
||||
assert any(item.get("state") == "SUCCEEDED" for item in items), "no succeeded task visible in console scope"
|
||||
assert any(item.get("handler", "").startswith("text.normalize.runtime.") for item in items)
|
||||
status, _ = request("/console/auth/logout", "POST", {})
|
||||
assert status == 200, status
|
||||
status, _ = request("/v1/tasks?" + urllib.parse.urlencode({"queue_id": queue, "limit": 1}))
|
||||
assert status == 401, status
|
||||
print("console_task_data=ok succeeded_tasks=%d logout=ok unauthorized_after_logout=%d" % (sum(item.get("state") == "SUCCEEDED" for item in items), status))
|
||||
PY
|
||||
|
||||
printf '%s\n' "console-e2e=passed image=$E2E_IMAGE url=$E2E_URL"
|
||||
Reference in New Issue
Block a user