diff --git a/sundynix-gateway/Dockerfile b/sundynix-gateway/Dockerfile index 4433741..8625b22 100644 --- a/sundynix-gateway/Dockerfile +++ b/sundynix-gateway/Dockerfile @@ -1,6 +1,19 @@ -# 第 2 层 业务网关。多阶段构建:编译静态二进制 → distroless 最小运行镜像。 -# 构建上下文须为「仓库根」(因 go.mod 用 replace ../sundynix-shared 引本地共享模块): +# 第 2 层 业务网关。多阶段构建:node 构建 admin 控制台 → Go 编译(内嵌 admin)→ distroless 运行。 +# 一个 gateway 容器同时 serve 控制台 UI + API + 供桌面端直连,无需单独 admin nginx 容器。 +# 构建上下文须为「仓库根」(因 go.mod 用 replace ../sundynix-shared,且需 COPY sundynix-admin/): # docker build -f sundynix-gateway/Dockerfile -t sundynix/gateway . + +# ── 0. 构建 admin 运维控制台前端 ── +FROM node:20-alpine AS admin +WORKDIR /app +COPY sundynix-admin/package*.json ./ +RUN npm config set registry https://registry.npmmirror.com && npm ci +COPY sundynix-admin/ ./ +# VITE_GATEWAY 留空 → 前端走相对 /api/v1,内嵌进 gateway 后同源、免跨域。 +ENV VITE_GATEWAY="" +RUN npm run build # 产物 /app/dist + +# ── 1. 编译 Go(go:embed 内嵌 admin 产物)── FROM golang:1.25-alpine AS build WORKDIR /src # 先拷依赖清单分层缓存(命中则跳过重下) @@ -14,8 +27,11 @@ WORKDIR /src COPY sundynix-shared/ ./sundynix-shared/ COPY sundynix-gateway/ ./sundynix-gateway/ WORKDIR /src/sundynix-gateway +# 用真实 admin 构建产物覆盖 embed 占位目录(go:embed 从 internal/webui/admin_dist 读) +COPY --from=admin /app/dist ./internal/webui/admin_dist RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/gateway ./cmd/server +# ── 2. 运行镜像 ── FROM gcr.io/distroless/static-debian12:nonroot COPY --from=build /out/gateway /gateway EXPOSE 8080 diff --git a/sundynix-gateway/internal/router/router.go b/sundynix-gateway/internal/router/router.go index d8038f6..7d55ff5 100644 --- a/sundynix-gateway/internal/router/router.go +++ b/sundynix-gateway/internal/router/router.go @@ -3,7 +3,9 @@ package router import ( "context" + "io/fs" "log" + "net/http" "os" "strings" @@ -16,6 +18,7 @@ import ( "github.com/sundynix/sundynix-gateway/internal/middleware" "github.com/sundynix/sundynix-gateway/internal/nats" "github.com/sundynix/sundynix-gateway/internal/store" + "github.com/sundynix/sundynix-gateway/internal/webui" ) // New 构建带有 Guardrail / 限流中间件的 Gin 引擎。 @@ -163,6 +166,31 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob. admin.GET("/guardrail-events", h.GuardrailEvents) // 护栏命中安全事件流(倒序,翻页) } } + + // ── 内嵌 admin 运维控制台:非 API 路径走 SPA ── + // admin 用 HashRouter,深链走 /#/...,服务端只会收到对 / 和 /assets/* 的请求: + // 命中真实文件直吐,其余回退根 index.html 交前端接管。/api/、/metrics、/healthz、 + // /readyz 都是已注册路由,永不进 NoRoute。embed 加载失败仅降级(控制台不可用),不影响 API。 + if adminDist, err := webui.Dist(); err != nil { + log.Printf("[gateway] admin 控制台静态资源加载失败(UI 不可用,API 不受影响): %v", err) + } else { + adminServer := http.FileServer(http.FS(adminDist)) + r.NoRoute(func(c *gin.Context) { + p := c.Request.URL.Path + if strings.HasPrefix(p, "/api/") { + c.JSON(http.StatusNotFound, gin.H{"error": "接口不存在"}) + return + } + if rel := strings.TrimPrefix(p, "/"); rel != "" { + if _, statErr := fs.Stat(adminDist, rel); statErr == nil { + adminServer.ServeHTTP(c.Writer, c.Request) // /assets/* 等真实文件 + return + } + } + c.Request.URL.Path = "/" // 其余回退 index.html + adminServer.ServeHTTP(c.Writer, c.Request) + }) + } return r } diff --git a/sundynix-gateway/internal/webui/admin_dist/index.html b/sundynix-gateway/internal/webui/admin_dist/index.html new file mode 100644 index 0000000..d7203b9 --- /dev/null +++ b/sundynix-gateway/internal/webui/admin_dist/index.html @@ -0,0 +1,16 @@ + + + + + + sundynix-agentix · 运维控制台 + + + +
+ admin 控制台尚未构建。请用 docker build -f sundynix-gateway/Dockerfile . + 构建(会自动 npm run build 并内嵌),或参见 deploy/132-app。 +
+ + diff --git a/sundynix-gateway/internal/webui/webui.go b/sundynix-gateway/internal/webui/webui.go new file mode 100644 index 0000000..30295f8 --- /dev/null +++ b/sundynix-gateway/internal/webui/webui.go @@ -0,0 +1,21 @@ +// Package webui 把 admin 运维控制台的前端构建产物打进 gateway 二进制。 +// admin_dist/ 默认只有占位页;Docker 多阶段构建会先 `npm run build` 出真实产物、 +// COPY 覆盖这里再编译 Go —— 于是一个 gateway 容器同时 serve 控制台 UI + API, +// 无需单独的 admin nginx 容器(部署见 deploy/132-app)。 +// +// 目录刻意命名 admin_dist 而非 dist:根 .gitignore(dist/) 与 .dockerignore(**/dist) +// 会忽略/排除 dist 目录,占位文件将无法提交、也进不了构建上下文。 +package webui + +import ( + "embed" + "io/fs" +) + +//go:embed all:admin_dist +var embeddedAdmin embed.FS + +// Dist 返回 admin 控制台静态文件系统(挂在根 /)。 +func Dist() (fs.FS, error) { + return fs.Sub(embeddedAdmin, "admin_dist") +}