#!/usr/bin/env bash # e2e-smoke.sh — L4 后端进程级端到端冒烟。 # # 真起打包好的 server 二进制(sqlite 临时库 + Redis + gRPC mTLS),跑一条真 HTTP/gRPC # over TCP/TLS 的全链路,验证 in-process 测试给不了的信心:二进制能起、迁移能跑、 # 路由/鉴权/DB 通、用量经 gRPC enroll+ReportUsage 真入库、再经 HTTP /v1/usage 真读出。 # # 用法: bash scripts/e2e-smoke.sh # 依赖: go · openssl · python3(随机端口+UUID) · curl # Redis 三选一:E2E_REDIS_ADDR(已有) > docker(redis:7-alpine) > 内嵌 miniredis # # 全部断言在 Go driver: server/test/e2e/smoke_test.go(build tag e2e): # HTTP 段(登录→/v1/me→/v1/usage) + gRPC 全链路(enroll→ReportUsage→/v1/usage bytes 断言)。 set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" TMP="$(mktemp -d)" REDIS_CID="" MR_PID="" SERVER_PID="" REDIS_ADDR_RESOLVED="" cleanup() { if [ -n "$SERVER_PID" ]; then kill "$SERVER_PID" 2>/dev/null || true; wait "$SERVER_PID" 2>/dev/null || true; fi if [ -n "$MR_PID" ]; then kill "$MR_PID" 2>/dev/null || true; wait "$MR_PID" 2>/dev/null || true; fi pkill -f 'test/e2e/miniredis' 2>/dev/null || true [ -n "$REDIS_CID" ] && docker rm -f "$REDIS_CID" >/dev/null 2>&1 || true rm -rf "$TMP" } trap cleanup EXIT rand_port() { python3 -c 'import socket;s=socket.socket();s.bind(("",0));print(s.getsockname()[1]);s.close()' } start_redis() { if [ -n "${E2E_REDIS_ADDR:-}" ]; then REDIS_ADDR_RESOLVED="$E2E_REDIS_ADDR"; echo "[e2e] 用已有 Redis $REDIS_ADDR_RESOLVED"; return fi if docker info >/dev/null 2>&1; then local p; p="$(rand_port)" REDIS_CID="$(docker run -d -p "$p:6379" redis:7-alpine)" REDIS_ADDR_RESOLVED="127.0.0.1:$p"; echo "[e2e] docker Redis $REDIS_ADDR_RESOLVED"; return fi echo "[e2e] 无 docker,用内嵌 miniredis 兜底 ..." ( cd server && go run -tags e2e ./test/e2e/miniredis ) > "$TMP/mr.addr" 2>"$TMP/mr.log" & MR_PID=$! for _ in $(seq 1 120); do [ -s "$TMP/mr.addr" ] && break; sleep 0.25; done REDIS_ADDR_RESOLVED="$(head -1 "$TMP/mr.addr" 2>/dev/null || true)" [ -n "$REDIS_ADDR_RESOLVED" ] || { echo "[e2e] ✗ miniredis 起失败:" >&2; cat "$TMP/mr.log" >&2; exit 1; } echo "[e2e] miniredis Redis $REDIS_ADDR_RESOLVED" } HTTP_PORT="$(rand_port)" GRPC_PORT="$(rand_port)" NODE_UUID="$(python3 -c 'import uuid;print(uuid.uuid4())')" start_redis echo "[e2e] 生成 JWT 密钥 + Node CA + gRPC server 证书 ..." openssl genrsa -out "$TMP/jwt_private.pem" 2048 2>/dev/null openssl rsa -in "$TMP/jwt_private.pem" -pubout -out "$TMP/jwt_public.pem" 2>/dev/null # Node CA(ECDSA P-256,mtls.NewCA 读它)+ gRPC server 证书(CA 签,SAN localhost+127.0.0.1) # —— 镜像 deploy/single-node/deploy.sh L119-142。 openssl ecparam -name prime256v1 -genkey -noout -out "$TMP/ca.key" 2>/dev/null openssl req -x509 -new -key "$TMP/ca.key" -days 3650 -out "$TMP/ca.crt" \ -subj "/O=Pangolin/CN=Pangolin Node CA" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" 2>/dev/null openssl ecparam -name prime256v1 -genkey -noout -out "$TMP/grpc.key" 2>/dev/null openssl req -new -key "$TMP/grpc.key" -out "$TMP/grpc.csr" -subj "/CN=pangolin-control-plane" 2>/dev/null cat > "$TMP/grpc.ext" </dev/null echo "[e2e] go build server + migrate + nodectl ..." ( cd server && go build -o "$TMP/server" ./cmd/server \ && go build -o "$TMP/migrate" ./cmd/migrate \ && go build -o "$TMP/nodectl" ./cmd/nodectl ) DB_FILE="$TMP/e2e.db" echo "[e2e] migrate(sqlite) ..." DB_DRIVER=sqlite DB_DSN="$DB_FILE" "$TMP/migrate" up echo "[e2e] 起 server HTTP :$HTTP_PORT gRPC :$GRPC_PORT ..." DB_DRIVER=sqlite DB_DSN="$DB_FILE" \ REDIS_ADDR="$REDIS_ADDR_RESOLVED" \ ADDR=":$HTTP_PORT" \ GRPC_ADDR=":$GRPC_PORT" \ CA_KEY_PATH="$TMP/ca.key" CA_CERT_PATH="$TMP/ca.crt" \ GRPC_CERT_PATH="$TMP/grpc.crt" GRPC_KEY_PATH="$TMP/grpc.key" \ JWT_PRIVATE_KEY_PATH="$TMP/jwt_private.pem" \ JWT_KEY_ID="e2e-key-1" \ JWT_PUBLIC_KEYS="e2e-key-1:$TMP/jwt_public.pem" \ WEBHOOK_SECRET="e2e-webhook-secret-000000000000000000" \ NODE_DERIVE_KEY="e2e-derive-key-0000000000000000000000" \ "$TMP/server" > "$TMP/server.log" 2>&1 & SERVER_PID=$! echo "[e2e] 等待 /healthz ..." ok=0 for _ in $(seq 1 40); do if curl -fsS "http://127.0.0.1:$HTTP_PORT/healthz" >/dev/null 2>&1; then ok=1; break; fi if ! kill -0 "$SERVER_PID" 2>/dev/null; then echo "[e2e] ✗ server 启动即退出,日志:" >&2; cat "$TMP/server.log" >&2; exit 1 fi sleep 0.5 done [ "$ok" = 1 ] || { echo "[e2e] ✗ /healthz 超时,日志:" >&2; cat "$TMP/server.log" >&2; exit 1; } echo "[e2e] 控制面健康。" echo "[e2e] 签发 agent bootstrap token(node=$NODE_UUID)..." TOKEN="$(REDIS_ADDR="$REDIS_ADDR_RESOLVED" "$TMP/nodectl" bootstrap-token -node="$NODE_UUID")" [ -n "$TOKEN" ] || { echo "[e2e] ✗ bootstrap token 签发失败" >&2; cat "$TMP/server.log" >&2; exit 1; } echo "[e2e] 跑 Go driver(HTTP + gRPC 全链路)..." ( cd server E2E_HTTP="http://127.0.0.1:$HTTP_PORT" \ E2E_DB_DSN="$DB_FILE" \ E2E_SERVER_LOG="$TMP/server.log" \ E2E_GRPC_ADDR="127.0.0.1:$GRPC_PORT" \ E2E_CA_CERT="$TMP/ca.crt" \ E2E_BOOTSTRAP_TOKEN="$TOKEN" \ E2E_NODE_UUID="$NODE_UUID" \ DB_DRIVER=sqlite \ go test -tags e2e ./test/e2e -run TestE2ESmoke -count=1 -v ) echo "[e2e] ✓ PASS — 进程级端到端冒烟通过(含 gRPC 全链路)"