feat(deploy): compose w/ nginx api-prefix ingress, scaled stateless api, e2e smoke

This commit is contained in:
2026-09-06 19:59:27 +08:00
parent a11bf28155
commit 94b215c2b3
7 changed files with 129 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
JWT_SECRET=change-me-openssl-rand-hex-32
ADMIN_USER=admin
ADMIN_PASSWORD=change-me-min-8
SCAN_INTERVAL_SEC=60
+13
View File
@@ -0,0 +1,13 @@
FROM golang:1.26-alpine AS build
WORKDIR /src
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY backend/ ./
RUN CGO_ENABLED=0 go build -trimpath -o /server ./cmd/server
FROM alpine:3.20
RUN adduser -D -H app
COPY --from=build /server /server
USER app
EXPOSE 8080
ENTRYPOINT ["/server"]
+3
View File
@@ -0,0 +1,3 @@
FROM nginx:1.27-alpine
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
COPY deploy/web-dist /usr/share/nginx/html
+18
View File
@@ -0,0 +1,18 @@
server {
listen 80;
client_max_body_size 200m;
resolver 127.0.0.11 valid=10s;
location /api/ {
set $upstream http://api:8080; # 变量式 → 每次按 DNS 解析,scale 后轮询到新副本
proxy_pass $upstream; # 无 URI 部分:保留 /api 前缀转发
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
root /usr/share/nginx/html;
try_files $uri /index.html;
}
}
+1
View File
@@ -0,0 +1 @@
<!doctype html><title>booklib</title><p>backend up — frontend lands in Plan 2.</p>
+34
View File
@@ -0,0 +1,34 @@
services:
web:
build: { context: ., dockerfile: deploy/Dockerfile.web }
ports: ["8080:80"]
depends_on: [api]
api:
build: { context: ., dockerfile: deploy/Dockerfile.api }
environment:
DATABASE_URL: postgres://lib:lib@postgres:5432/lib?sslmode=disable
REDIS_URL: redis://redis:6379
JWT_SECRET: ${JWT_SECRET}
ADMIN_USER: ${ADMIN_USER}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
BOOKS_DIR: /data/books
CACHE_DIR: /data/cache
SCAN_INTERVAL_SEC: ${SCAN_INTERVAL_SEC:-60}
volumes:
- ./library:/data/books
- cache:/data/cache
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_started }
postgres:
image: postgres:16-alpine
environment: { POSTGRES_USER: lib, POSTGRES_PASSWORD: lib, POSTGRES_DB: lib }
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck: { test: ["CMD-SHELL", "pg_isready -U lib"], interval: 2s, timeout: 2s, retries: 30 }
redis:
image: redis:7-alpine
command: ["redis-server", "--maxmemory", "128mb", "--maxmemory-policy", "allkeys-lru"]
# 故意无 volume:redis 里全是可再生数据(spec §6.2)
volumes:
pgdata:
cache:
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
BASE=${BASE:-http://localhost:8080}
API=$BASE/api
J=(-H 'content-type: application/json')
[ -f .env ] && set -a && . ./.env && set +a
say(){ echo "smoke: $1"; }
die(){ echo "SMOKE FAIL: $1"; exit 1; }
tokfor(){ curl -fsS "$API/auth/login" "${J[@]}" -d "{\"username\":\"$1\",\"password\":\"$2\"}" | sed -E 's/.*"token":"([^"]+)".*/\1/'; }
say "healthz"
curl -fsS "$API/healthz" >/dev/null || die "healthz down"
say "login"
TOK=$(tokfor "$ADMIN_USER" "$ADMIN_PASSWORD")
[ -n "$TOK" ] || die "no token"
AUTH="authorization: Bearer $TOK"
say "member user + role enforcement"
curl -fsS "$API/users" "${J[@]}" -H "$AUTH" -d '{"username":"smoke","password":"smokepw123","role":"member"}' >/dev/null || die "create member"
MTOK=$(tokfor smoke smokepw123)
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$API/users" "${J[@]}" -H "authorization: Bearer $MTOK" -d '{"username":"x","password":"xpw12345","role":"member"}')
[ "$code" = 403 ] || die "member write not blocked ($code)"
say "library + bad-ext upload rejected + good upload + scan"
mkdir -p library/smoke-books
LID=$(curl -fsS "$API/libraries" "${J[@]}" -H "$AUTH" -d '{"name":"smoke","root_path":"/data/books/smoke-books"}' | sed -E 's/.*"id":([0-9]+).*/\1/')
printf 'x' > library_upload_note.txt
curl -fsS -o /dev/null "$API/libraries/$LID/upload" -H "$AUTH" -F "file=@library_upload_note.txt;filename=virus.exe" && die "bad ext upload must fail" || true
printf 'hello smoke book' > library_upload_note.txt
curl -fsS -o /dev/null "$API/libraries/$LID/upload" -H "$AUTH" -F "file=@library_upload_note.txt;filename=note.txt" || die "upload failed"
curl -fsS -o /dev/null -X POST "$API/libraries/$LID/scan" -H "$AUTH" || die "scan trigger"
found=""
for _ in $(seq 30); do
if curl -fsS "$API/books?library=$LID" -H "$AUTH" | grep -q '"path":"note.txt"'; then found=1; break; fi
sleep 1
done
[ -n "$found" ] || die "book not indexed after 30s"
say "read + progress roundtrip"
BID=$(curl -fsS "$API/books?library=$LID" -H "$AUTH" | sed -E 's/.*"id":([0-9]+).*/\1/')
curl -fsS "$API/books/$BID/file" -H "$AUTH" | grep -q "hello smoke book" || die "file body"
code=$(curl -s -o /dev/null -w '%{http_code}' -X PUT "$API/books/$BID/progress" "${J[@]}" -H "authorization: Bearer $MTOK" -d '{"locator":{"scroll":0.5},"percent":0.5}')
[ "$code" = 204 ] || die "progress put $code"
curl -fsS "$API/progress" -H "authorization: Bearer $MTOK" | grep -q '"percent":0.5' || die "progress read"
say "immutable cache header"
COVER=$(curl -fsS "$API/books/$BID" -H "$AUTH" | sed -E 's/.*"cover_url":"([^"]+)".*/\1/')
curl -fsS -o /dev/null -D - "$BASE$COVER" -H "$AUTH" | grep -qi 'cache-control:.*immutable' || die "cover not immutable"
say "delete book → file gone from host dir"
curl -fsS -o /dev/null -X DELETE "$API/books/$BID" -H "$AUTH" || die "delete"
[ ! -f library/smoke-books/note.txt ] || die "file survived delete"
say "ALL SMOKE TESTS PASSED"