diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md deleted file mode 100644 index 8393fa0..0000000 --- a/.planning/PROJECT.md +++ /dev/null @@ -1,88 +0,0 @@ -# Fengling Gateway - -## 这是什么 - -基于 YARP (Yet Another Reverse Proxy) 的 API 网关,用于风灵微服务生态系统。支持多租户路由、动态配置和分布式负载均衡,将请求路由到下游服务(auth-service、member-service、activity、platform、risk-control 等)。 - -## 核心价值 - -可靠、可扩展的 API 网关,将流量分发到后端微服务,支持零停机配置更新。 - -## 需求 - -### 已验证(现有功能) - -- ✓ 基于 URL 路径的多租户路由 — 已有 -- ✓ JWT Token 解析和租户声明提取 — 已有 -- ✓ PostgreSQL 动态路由配置 — 已有 -- ✓ Kubernetes 服务发现集成 — 已有 -- ✓ 加权轮询负载均衡 — 已有 -- ✓ 通过 PostgreSQL NOTIFY 实现配置热重载 — 已有 - -### 进行中 - -- [ ] 实现 console 驱动的配置管理(配置在 fengling-console 变更,网关监听并重载) -- [ ] 通过 PostgreSQL NOTIFY 广播支持多网关实例部署 -- [ ] 将 K8s 健康检查从网关移除(委托给 console) - -### 范围外 - -- [直接配置网关的 UI] — 由 fengling-console 负责 -- [网关中的 K8s 服务健康检查] — 委托给 console -- [认证/授权逻辑] — 由 auth-service 负责 - -## 背景 - -**生态系统结构:** -``` -fengling-gateway/ # 当前项目 - API 网关 (YARP) - ↓ 路由流量到: -fengling-console/ # 中央管理控制台 - 配置、租户管理 -fengling-console-web/ # 控制台 Web UI -fengling-auth-service/ # 认证服务 -fengling-member-service/ # 会员服务 -fengling-activity/ # 活动服务 -fengling-platform/ # 平台服务 -fengling-risk-control/ # 风控服务 -fengling-service-discovery/# 服务发现 -``` - -**架构决策(新):** -- 网关配置由 fengling-console 管理,网关不直接配置 -- Console 发布配置变更 → 网关订阅并重载 -- 多实例支持通过 PostgreSQL NOTIFY/LISTEN 实现(更轻量,无需 Redis) -- Console 负责所有 K8s 服务健康监控 -- 网关只处理请求路由 - -**当前问题(来自 CONCERNS.md):** -- 硬编码凭据(安全风险) -- JWT Token 未验证(安全风险) -- API 端点无认证(安全风险) -- 负载均衡锁竞争 -- 缺少单元测试 - -**Console 集成现状:** -- fengling-console 已实现 GatewayController 和 GatewayService -- Console 拥有 GatewayDbContext,可直接管理网关配置数据 -- Console 的 ReloadGatewayAsync() 目前为空实现,未实现广播机制 -- 网关已有 PgSqlConfigChangeListener 使用 NOTIFY/LISTEN,可复用 - -## 约束 - -- **多实例**:网关必须支持同时运行多个实例 -- **热重载**:配置变更无需重启 -- **技术栈**:.NET 10.0, YARP, PostgreSQL -- **部署**:Docker + Kubernetes - -## 关键决策 - -| 决策 | 理由 | 结果 | -|------|------|------| -| Console 驱动配置 | 集中管理,单一事实来源 | ✓ 良好 | -| PostgreSQL NOTIFY 广播 | 轻量方案,无需额外依赖 | ✓ 良好 | -| K8s 健康委托给 console | 降低网关复杂度,console 是运维中心 | ✓ 良好 | -| 保持 YARP 为核心 | 微软维护,支持良好 | ✓ 良好 | - ---- - -*最后更新:2026-03-02 初始化后* diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md deleted file mode 100644 index 2c3f050..0000000 --- a/.planning/REQUIREMENTS.md +++ /dev/null @@ -1,102 +0,0 @@ -# 需求文档:Fengling Gateway - -**定义日期:** 2026-03-02 -**核心价值:** 可靠、可扩展的 API 网关,将流量分发到后端微服务,支持零停机配置更新。 - -## v1 需求 - -初始发布版本的需求。每个需求对应一个 Roadmap 阶段。 - -### 配置管理 - -- [x] **CFG-01**:网关监听来自 fengling-console 的配置变更事件(PostgreSQL NOTIFY) -- [x] **CFG-02**:收到通知后网关无需重启即可重载配置 -- [x] **CFG-03**:多实例网关通过 PostgreSQL NOTIFY 广播接收配置更新 - -### 多实例支持 - -- [x] **INST-01**:多个网关实例可以同时运行 -- [x] **INST-02**:配置变更通过 NOTIFY 广播传播到所有实例 -- [x] **INST-03**:使用 PostgreSQL LISTEN 订阅配置变更频道 - -### K8s 健康委托 - -- [x] **K8S-01**:从网关注销 K8s 健康监控 -- [x] **K8S-02**:网关将服务健康检查委托给 console - -- [ ] **K8S-01**:从网关注销 K8s 健康监控 -- [ ] **K8S-02**:网关将服务健康检查委托给 console - -### 安全修复 - -- [ ] **SEC-01**:移除源代码中的硬编码凭据 -- [ ] **SEC-02**:实现正确的 JWT Token 验证 -- [ ] **SEC-03**:为网关管理 API 端点添加认证 - -### 性能优化 - -- [ ] **PERF-01**:优化负载均衡锁竞争 -- [ ] **PERF-02**:实现增量路由缓存更新 - -## v2 需求 - -延期到未来版本。已记录但不在当前 Roadmap 中。 - -### 可观测性 - -- **OBS-01**:分布式追踪集成 -- **OBS-02**:网关性能自定义指标 - -### 测试 - -- **TEST-01**:RouteCache 单元测试 -- **TEST-02**:JwtTransformMiddleware 单元测试 -- **TEST-03**:负载均衡策略单元测试 - -## 范围外 - -| 功能 | 原因 | -|------|------| -| 直接配置网关的 UI | 由 fengling-console 处理 | -| 网关中的 K8s 服务健康检查 | 委托给 console | -| 网关中的认证逻辑 | 由 auth-service 处理 | -| 网关中的授权逻辑 | 由下游服务处理 | - -## 可追溯性 - -哪些阶段覆盖哪些需求。Roadmap 创建时更新。 - -| 需求 | 阶段 | 状态 | -|------|------|------| -| CFG-01 | 阶段 1 | ✅ 已完成 | -| CFG-02 | 阶段 1 | ✅ 已完成 | -| CFG-03 | 阶段 1 | ✅ 已完成 | -| INST-01 | 阶段 1 | ✅ 已完成 | -| INST-02 | 阶段 1 | ✅ 已完成 | -| INST-03 | 阶段 1 | ✅ 已完成 | -QH|| K8S-01 | 阶段 2 | ✅ 已完成 | -BH|| K8S-02 | 阶段 2 | ✅ 已完成 | -| K8S-02 | 阶段 2 | 待处理 | -| SEC-01 | 阶段 3 | 待处理 | -| SEC-02 | 阶段 3 | 待处理 | -| SEC-03 | 阶段 3 | 待处理 | -| PERF-01 | 阶段 4 | 待处理 | -| PERF-02 | 阶段 4 | 待处理 | - -**覆盖率:** -- v1 需求:共 12 项 -- 已映射到阶段:12 项 -- 未映射:0 ✓ -- 已完成:8 项(阶段 1 + 阶段 2) -- 待处理:4 项 -- 待处理:6 项 - ---- - -**阶段 1 完成说明:** -- 现有 `PgSqlConfigChangeListener.cs` 已实现所有监听需求 -- 监听频道:`gateway_config_changed` -- 包含断线重连和回退轮询机制 - -*需求定义:2026-03-02* -*最后更新:2026-03-02 阶段1完成后* diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md deleted file mode 100644 index 46b51ce..0000000 --- a/.planning/ROADMAP.md +++ /dev/null @@ -1,198 +0,0 @@ -#MY|# Roadmap:Fengling Gateway - -**创建日期:** 2026-03-02 -**核心价值:** 可靠、可扩展的 API 网关,将流量分发到后端微服务,支持零停机配置更新。 - ---- - -## 阶段 1:配置变更监听与多实例支持 ✅ 已完成 - -**目标:** 实现网关对配置变更的监听机制,支持多实例部署。 - -> **注意:** 此阶段只实现 YARP 网关部分的监听代码。Console 端的广播通知由 fengling-console 项目负责。 - -**需求:** -- [x] CFG-01:网关监听来自 fengling-console 的配置变更事件(PostgreSQL NOTIFY) -- [x] CFG-02:收到通知后网关无需重启即可重载配置 -- [x] CFG-03:多实例网关通过 PostgreSQL NOTIFY 广播接收配置更新 -- [x] INST-01:多个网关实例可以同时运行 -- [x] INST-02:配置变更通过 NOTIFY 广播传播到所有实例 -- [x] INST-03:使用 PostgreSQL LISTEN 订阅配置变更频道 - -**成功标准:** -- [x] 网关使用 LISTEN 订阅配置变更频道(如 `gateway_config_changed`) -- [x] 收到 NOTIFY 后触发配置重载,无需重启 -- [x] 多个网关实例通过数据库 NOTIFY 保持同步 -- [x] 广播事件在 5 秒内到达所有实例 - -**实现文件:** -- `src/yarpgateway/Services/PgSqlConfigChangeListener.cs` - ---- - -## 阶段 2:K8s 健康检查委托 ✅ 已完成 - -**目标:** 将 K8s 服务健康监控从网关移除,委托给 fengling-console。 - -**需求:** -- [x] K8S-01:从网关注销 K8s 健康监控 -- [x] K8S-02:网关将服务健康检查委托给 console - -**成功标准:** -- [x] KubernetesPendingSyncService 已从网关移除 -- [x] PendingServicesController 已从网关移除 -- [x] 网关只执行请求路由,不做健康监控 - -**已删除的文件:** -- `Services/KubernetesPendingSyncService.cs` -- `Controllers/PendingServicesController.cs` -- `Models/GwPendingServiceDiscovery.cs` - -**已修改的文件:** -- `Program.cs` - 移除服务注册和 using 语句 -- `Data/GatewayDbContext.cs` - 移除 DbSet 和模型配置 - ---- - -## 阶段 3:安全加固 - -**目标:** 修复关键安全漏洞。 - -**需求:** -- [ ] SEC-01:移除源代码中的硬编码凭据 -- [ ] SEC-02:实现正确的 JWT Token 验证 -- [ ] SEC-03:为网关管理 API 端点添加认证 - -**成功标准:** -1. 源代码中无硬编码密码/密钥 -2. JWT Token 经过验证(签名、过期时间、颁发者、受众) -3. 所有 /api/gateway/* 端点需要认证 - ---- - -## 阶段 4:性能优化 - -**目标:** 优化高负载下的网关性能。 - -**需求:** -- [ ] PERF-01:优化负载均衡锁竞争 -- [ ] PERF-02:实现增量路由缓存更新 - -**成功标准:** -1. 负载均衡不需要每个请求都获取 Redis 锁 -2. 路由缓存更新是增量式的,而非全量重载 -3. 网关处理能力提升 10 倍 - ---- - -## 阶段 5:可观测性与测试 - -**目标:** 添加可观测性和测试覆盖。 - -**需求:** -- [ ] OBS-01:分布式追踪集成 -- [ ] OBS-02:网关性能自定义指标 -- [ ] TEST-01:RouteCache 单元测试 -- [ ] TEST-02:JwtTransformMiddleware 单元测试 -- [ ] TEST-03:负载均衡策略单元测试 - -**成功标准:** -1. 分布式追踪包含网关跨度 -2. 导出关键指标(请求数、延迟、错误率) -3. 核心组件测试覆盖率 >80% - ---- - -BZ|## 阶段 6:网关插件技术调研与实现 ✅ 进行中 - -**目标:** 实现网关插件化支持。 - -**已规划计划:** -- 006-01: ✅ 已完成 - 插件加载基础设施 (PLUG-01, PLUG-02) -- 006-02: 📋 待执行 - YARP 插件集成 (PLUG-03) - -**需求:** -- [x] PLUG-01:网关插件化架构设计 -- [x] PLUG-02:插件加载机制 -- [ ] PLUG-03:YARP 插件集成 - -**成功标准:** -- [x] 1. 网关支持动态加载插件 -- [x] 2. 插件之间相互隔离 -- [ ] 3. 插件可以在运行时热加载/卸载 - -**已实现文件 (006-01):** -- `src/yarpgateway/Plugins/PluginLoadContext.cs` - ALC 隔离 -- `src/yarpgateway/Plugins/PluginLoader.cs` - 发现和加载 -- `src/yarpgateway/Plugins/PluginHost.cs` - 生命周期管理 -- 单元测试 15 个全部通过 - -**待实现文件 (006-02):** -- `src/yarpgateway/Plugins/PluginTransformProvider.cs` - YARP Transform 提供者 -- `src/yarpgateway/Plugins/DestinationSelector.cs` - 目标选择器 -- `src/yarpgateway/Plugins/PluginConfigWatcher.cs` - Console DB 通知监听 - -**目标:** 实现网关插件化支持。 - -**需求:** -- [x] PLUG-01:网关插件化架构设计 -- [x] PLUG-02:插件加载机制 -- [ ] PLUG-03:插件隔离与生命周期管理 - -**成功标准:** -- [x] 1. 网关支持动态加载插件 -- [x] 2. 插件之间相互隔离 -- [ ] 3. 插件可以在运行时热加载/卸载 - -**已实现文件:** -- `src/yarpgateway/Plugins/PluginLoadContext.cs` - ALC 隔离 -- `src/yarpgateway/Plugins/PluginLoader.cs` - 发现和加载 -- `src/yarpgateway/Plugins/PluginHost.cs` - 生命周期管理 -- 单元测试 15 个全部通过 - ---- - -## Roadmap 摘要 - -| 阶段 | 名称 | 需求数 | 状态 | -|------|------|--------|------| -| 1 | 配置变更监听与多实例支持 | 6 | ✅ 已完成 | -| 2 | K8s 健康检查委托 | 2 | ✅ 已完成 | -| 3 | 安全加固 | 3 | 未规划 | -| 4 | 性能优化 | 2 | 未规划 | -| 5 | 可观测性与测试 | 5 | 未规划 | -| 6 | 网关插件技术调研与实现 | 3 | ✅ 进行中 | - -## 阶段 7:网关配置重构规划 - -**目标:** 分析网关配置新想法的可行性,重新规划网关配置架构。 - -**需求:** -- [ ] REPL-01:分析"网关配置的新想法.md"中的方案 -- [ ] REPL-02:识别与现有需求的冲突点 -- [ ] REPL-03:制定新的网关配置架构 - -**成功标准:** -1. 完成网关配置新想法的可行性分析 -2. 提出与现有阶段兼容的配置方案 -3. 更新 roadmap 以反映新的配置架构 - ---- - -## Roadmap 摘要 - -| 阶段 | 名称 | 需求数 | 状态 | -|------|------|--------|------| -| 1 | 配置变更监听与多实例支持 | 6 | ✅ 已完成 | -| 2 | K8s 健康检查委托 | 2 | ✅ 已完成 | -| 3 | 安全加固 | 3 | 未规划 | -| 4 | 性能优化 | 2 | 未规划 | -| 5 | 可观测性与测试 | 5 | 未规划 | -| 6 | 网关插件技术调研与实现 | 3 | ✅ 进行中 | -| 7 | 网关配置重构规划 | 3 | 待规划 | - -**总计:** 7 个阶段 | 24 个需求 | 8 项已完成 - ---- - -*最后更新:2026-03-04 阶段6已完成 PLUG-01 和 PLUG-02* diff --git a/.planning/STATE.md b/.planning/STATE.md deleted file mode 100644 index 5c861c8..0000000 --- a/.planning/STATE.md +++ /dev/null @@ -1,128 +0,0 @@ -#VR|# 状态:Fengling Gateway - -**最后更新:** 2026-03-04 - ---- - -## 项目引用 - -参考:.planning/PROJECT.md(更新于 2026-03-02) - -**核心价值:** 可靠、可扩展的 API 网关,将流量分发到后端微服务,支持零停机配置更新。 - -**当前重点:** 阶段 6:网关插件技术调研与实现 - ---- - -## 项目状态 - -| 项目 | 状态 | -|------|------| -| PROJECT.md | ✓ 已初始化 | -| config.json | ✓ 已创建 | -| 需求文档 | ✓ 已定义(18 个需求) | -| Roadmap | ✓ 已创建(6 个阶段) | -| 研究 | 未开始(自动模式跳过) | - ---- - -## 阶段状态 - -| 阶段 | 名称 | 状态 | 计划数 | 进度 | -|------|------|------|--------|------| -| 1 | 配置变更监听与多实例支持 | ✅ 已完成 | 0 | 100% | -| 2 | K8s 健康检查委托 | ✅ 已完成 | 0 | 100% | -| 3 | 安全加固 | 未规划 | 0 | 0% | -| 4 | 性能优化 | 未规划 | 0 | 0% | -| 5 | 可观测性与测试 | 未规划 | 0 | 0% | -#NH|QJ|| 6 | 网关插件技术调研与实现 | ✅ 进行中 | 2 | 50% | -#PW|| 7 | 网关配置重构规划 | 待规划 | 0 | 0% | - ---- - -## 累积上下文 - -### 初始化 - -- **2026-03-02:** 通过 /gsd-new-project --auto 初始化项目 -- 现有代码库的重构项目(已存在 ARCHITECTURE.md、CONCERNS.md、STACK.md) -- 用户提供背景:网关架构讨论,重点是 console 驱动的配置管理 - -### 关键决策 - -| 决策 | 日期 | 备注 | -|------|------|------| -| Console 驱动配置 | 2026-03-02 | 配置在 fengling-console 变更,网关监听 | -| PostgreSQL NOTIFY 广播 | 2026-03-02 | 使用 PostgreSQL NOTIFY/LISTEN,更轻量 | -| K8s 健康委托 | 2026-03-02 | Console 处理 K8s 健康,非网关 | - -### 阶段 1 分析结论 - -- **2026-03-02:** 分析现有代码 `PgSqlConfigChangeListener.cs` -- 结论:现有实现已完整满足阶段 1 所有需求 -- 监听频道:`gateway_config_changed` -- 包含:断线重连、回退轮询(5分钟) - -### Console 集成现状 - -- Console 已实现 GatewayController 和 GatewayService -- Console 拥有 GatewayDbContext,可直接管理网关配置 -- ReloadGatewayAsync() 为空实现,需要在 fengling-console 中实现 NOTIFY 发送 - -SK|### 阶段 6 实施 - -**2026-03-04:** 实现插件加载基础设施 -- 完成 PLUG-01:网关插件化架构设计 -- 完成 PLUG-02:插件加载机制 -- 实现文件: - - `src/yarpgateway/Plugins/PluginLoadContext.cs` - - `src/yarpgateway/Plugins/PluginLoader.cs` - - `src/yarpgateway/Plugins/PluginHost.cs` -- 单元测试:15 个全部通过 - -**2026-03-04:** YARP 集成设计决策(与用户讨论) -- 管道选择:Transforms(更轻量,少资源占用) -- OnRouteMatchedAsync:用于目标选择(特殊租户访问特殊目标) -- 负载均衡:暂不考虑,使用内置 -- 插件启用:Metadata + Console DB 通知触发 -- 插件排序:需要(通过 PluginOrder metadata) - -**已规划计划 006-02:** -- PluginTransformProvider - YARP Transform 提供者 -- DestinationSelector - 目标选择器 -- PluginConfigWatcher - Console DB 通知监听 - -XX|--- - - - -- **2026-03-04:** 实现插件加载基础设施 -- 完成 PLUG-01:网关插件化架构设计 -- 完成 PLUG-02:插件加载机制 -- 实现文件: - - `src/yarpgateway/Plugins/PluginLoadContext.cs` - - `src/yarpgateway/Plugins/PluginLoader.cs` - - `src/yarpgateway/Plugins/PluginHost.cs` -- 单元测试:15 个全部通过 - -### Roadmap Evolution - -- **2026-03-07:** 阶段 7 添加:网关配置重构规划(分析"网关配置的新想法.md"中的方案,识别冲突点,制定新架构) - -## 快速任务完成 - -| # | Description | Date | Commit | Directory | -|---|-------------|------|--------|----------| -| 001 | 升级 Fengling.Platform 包并修复编译警告 | 2026-03-04 | 42b8c9c | [001-upgrade-platform](./quick/001-upgrade-platform/) | - ---- - -## 备注 - -- 自动模式:跳过研究,工作流偏好设置为 yolo -- 配置变更应提交到 git(commit_docs: true) -- gsd-tools.cjs 不可用 - 项目结构手动创建 - ---- - -*最后更新:2026-03-04 - 完成阶段 6 计划 006-01:插件加载基础设施(PLUG-01, PLUG-02)* diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md deleted file mode 100644 index ca999ef..0000000 --- a/.planning/codebase/ARCHITECTURE.md +++ /dev/null @@ -1,457 +0,0 @@ -# YARP Gateway 架构文档 - -## 1. 整体架构模式 - -本项目基于 **YARP (Yet Another Reverse Proxy)** 实现的 API 网关,采用 **反向代理模式**,支持多租户路由、动态配置和分布式负载均衡。 - -### 1.1 架构图 - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 外部请求 │ -└─────────────────────────────────┬───────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ ASP.NET Core Pipeline │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ ┌────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐ │ -│ │ CORS 中间件 │ -> │ JwtTransformMiddleware │ -> │ TenantRoutingMiddleware │ │ -│ └────────────────┘ └─────────────────────┘ └──────────────────────┘ │ -└─────────────────────────────────┬───────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ YARP Reverse Proxy │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ ┌───────────────────────────┐ ┌──────────────────────────────────────┐ │ -│ │ DynamicProxyConfigProvider │ -> │ DistributedWeightedRoundRobinPolicy │ │ -│ └───────────┬───────────────┘ └──────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌───────────────────────────────────────────────────────────────────────┐ │ -│ │ RouteConfig / ClusterConfig │ │ -│ └───────────────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────┬───────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 后端服务集群 │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ Service A│ │ Service B│ │ Service C│ │ Service D│ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### 1.2 核心设计模式 - -| 模式 | 应用场景 | 实现位置 | -|------|----------|----------| -| 反向代理 | 请求转发 | `Yarp.ReverseProxy` | -| 策略模式 | 负载均衡策略 | `DistributedWeightedRoundRobinPolicy` | -| 观察者模式 | 配置变更监听 | `PgSqlConfigChangeListener` | -| 工厂模式 | DbContext 创建 | `GatewayDbContextFactory` | -| 单例模式 | 配置提供者 | `DatabaseRouteConfigProvider`, `DatabaseClusterConfigProvider` | -| 生产者-消费者 | 配置变更通知 | `Channel` in `PgSqlConfigChangeListener` | - ---- - -## 2. 核心组件和职责 - -### 2.1 中间件层 (Middleware) - -#### JwtTransformMiddleware -**文件路径**: `src/Middleware/JwtTransformMiddleware.cs` - -**职责**: -- 解析 JWT Token -- 提取租户信息 (tenant claim) -- 将用户信息注入请求头 - -**处理流程**: -``` -Authorization Header -> JWT 解析 -> 提取 Claims -> 注入 X-Tenant-Id, X-User-Id, X-User-Name, X-Roles -``` - -#### TenantRoutingMiddleware -**文件路径**: `src/Middleware/TenantRoutingMiddleware.cs` - -**职责**: -- 从请求头获取租户 ID -- 根据 URL 路径提取服务名称 -- 查询路由缓存获取目标集群 -- 设置动态集群 ID - -### 2.2 配置提供层 (Config Providers) - -#### DynamicProxyConfigProvider -**文件路径**: `src/DynamicProxy/DynamicProxyConfigProvider.cs` - -**职责**: -- 实现 YARP 的 `IProxyConfigProvider` 接口 -- 整合路由和集群配置 -- 提供配置变更通知机制 - -```csharp -public interface IProxyConfigProvider -{ - IProxyConfig GetConfig(); -} -``` - -#### DatabaseRouteConfigProvider -**文件路径**: `src/Config/DatabaseRouteConfigProvider.cs` - -**职责**: -- 从数据库加载路由配置 -- 转换为 YARP `RouteConfig` 格式 -- 支持热重载 - -#### DatabaseClusterConfigProvider -**文件路径**: `src/Config/DatabaseClusterConfigProvider.cs` - -**职责**: -- 从数据库加载集群配置 -- 管理服务实例 (地址、权重) -- 配置健康检查策略 - -### 2.3 服务层 (Services) - -#### RouteCache -**文件路径**: `src/Services/RouteCache.cs` - -**职责**: -- 内存缓存路由信息 -- 支持全局路由和租户专用路由 -- 提供快速查询接口 - -**数据结构**: -``` -_globalRoutes: ConcurrentDictionary // 全局路由 -_tenantRoutes: ConcurrentDictionary> // 租户路由 -``` - -**查询优先级**: 租户专用路由 > 全局路由 - -#### PgSqlConfigChangeListener -**文件路径**: `src/Services/PgSqlConfigChangeListener.cs` - -**职责**: -- 监听 PostgreSQL NOTIFY 事件 -- 双重保障:事件监听 + 轮询回退 -- 触发配置热重载 - -**监听流程**: -``` -PostgreSQL NOTIFY -> OnNotification -> _reloadChannel -> ReloadConfigAsync - │ - └── FallbackPollingAsync (5分钟轮询) -``` - -#### KubernetesPendingSyncService -**文件路径**: `src/Services/KubernetesPendingSyncService.cs` - -**职责**: -- 同步 Kubernetes 服务发现 -- 管理待处理服务列表 -- 清理过期服务记录 - -#### RedisConnectionManager -**文件路径**: `src/Services/RedisConnectionManager.cs` - -**职责**: -- 管理 Redis 连接 -- 提供分布式锁实现 -- 连接池管理 - -### 2.4 负载均衡层 - -#### DistributedWeightedRoundRobinPolicy -**文件路径**: `src/LoadBalancing/DistributedWeightedRoundRobinPolicy.cs` - -**职责**: -- 实现加权轮询负载均衡 -- 基于 Redis 的分布式状态存储 -- 支持实例权重配置 - -**算法流程**: -``` -1. 获取分布式锁 (Redis) -2. 读取负载均衡状态 -3. 计算权重选择目标 -4. 更新状态并释放锁 -5. 失败时降级到简单选择 -``` - ---- - -## 3. 数据流和请求处理流程 - -### 3.1 请求处理流程图 - -```mermaid -sequenceDiagram - participant Client as 客户端 - participant CORS as CORS中间件 - participant JWT as JwtTransformMiddleware - participant Tenant as TenantRoutingMiddleware - participant YARP as YARP代理 - participant LB as 负载均衡器 - participant Service as 后端服务 - - Client->>CORS: HTTP请求 - CORS->>JWT: 跨域检查通过 - JWT->>JWT: 解析JWT Token - JWT->>Tenant: 注入租户信息头 - Tenant->>Tenant: 提取服务名称 - Tenant->>Tenant: 查询RouteCache - Tenant->>YARP: 设置动态集群ID - YARP->>LB: 获取可用目标 - LB->>LB: 加权轮询选择 - LB->>Service: 转发请求 - Service-->>Client: 返回响应 -``` - -### 3.2 配置变更流程 - -```mermaid -flowchart TD - A[数据库变更] --> B[SaveChangesAsync] - B --> C[DetectConfigChanges] - C --> D[NOTIFY gateway_config_changed] - D --> E[PgSqlConfigChangeListener] - E --> F{收到通知?} - F -->|是| G[ReloadConfigAsync] - F -->|否| H[轮询检测版本变化] - H --> G - G --> I[RouteCache.ReloadAsync] - G --> J[DatabaseRouteConfigProvider.ReloadAsync] - G --> K[DatabaseClusterConfigProvider.ReloadAsync] - I --> L[更新内存缓存] - J --> L - K --> L - L --> M[DynamicProxyConfigProvider.UpdateConfig] - M --> N[触发 IChangeToken] - N --> O[YARP重新加载配置] -``` - -### 3.3 Kubernetes 服务发现流程 - -``` -┌─────────────────┐ -│ Kubernetes API │ -└────────┬────────┘ - │ 30s 间隔 - ▼ -┌─────────────────────────────┐ -│ KubernetesPendingSyncService │ -├─────────────────────────────┤ -│ 1. 获取 K8s 服务列表 │ -│ 2. 对比现有待处理记录 │ -│ 3. 新增/更新/清理记录 │ -└────────┬────────────────────┘ - │ - ▼ -┌─────────────────────────────┐ -│ GwPendingServiceDiscovery │ -│ (待处理服务发现表) │ -└────────┬────────────────────┘ - │ - ▼ -┌─────────────────────────────┐ -│ PendingServicesController │ -│ - GET: 查看待处理服务 │ -│ - POST /assign: 分配集群 │ -│ - POST /reject: 拒绝服务 │ -└─────────────────────────────┘ -``` - ---- - -## 4. 关键抽象层 - -### 4.1 配置模型 - -``` -┌───────────────────────────────────────────────────────────────┐ -│ 配置层次结构 │ -├───────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ JwtConfig │ │ RedisConfig │ │ -│ │ - Authority │ │ - Connection │ │ -│ │ - Audience │ │ - Database │ │ -│ │ - Validate* │ │ - InstanceName │ │ -│ └─────────────────┘ └─────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ DynamicProxyConfigProvider │ │ -│ │ ┌─────────────────┐ ┌─────────────────┐ │ │ -│ │ │ RouteConfig[] │ │ ClusterConfig[] │ │ │ -│ │ │ - RouteId │ │ - ClusterId │ │ │ -│ │ │ - ClusterId │ │ - Destinations │ │ │ -│ │ │ - Match.Path │ │ - LoadBalancing │ │ │ -│ │ │ - Metadata │ │ - HealthCheck │ │ │ -│ │ └─────────────────┘ └─────────────────┘ │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -└───────────────────────────────────────────────────────────────┘ -``` - -### 4.2 数据模型 - -``` -┌─────────────────┐ ┌─────────────────┐ -│ GwTenant │ │ GwTenantRoute │ -├─────────────────┤ ├─────────────────┤ -│ Id │ │ Id │ -│ TenantCode ────┼────►│ TenantCode │ -│ TenantName │ │ ServiceName │ -│ Status │ │ ClusterId │ -│ Version │ │ PathPattern │ -│ IsDeleted │ │ Priority │ -└─────────────────┘ │ IsGlobal │ - │ Status │ - │ Version │ - └────────┬────────┘ - │ - ▼ - ┌─────────────────┐ - │ GwServiceInstance│ - ├─────────────────┤ - │ Id │ - │ ClusterId ────┤ - │ DestinationId │ - │ Address │ - │ Health │ - │ Weight │ - │ Status │ - │ Version │ - └─────────────────┘ -``` - -### 4.3 接口定义 - -```csharp -// 路由缓存接口 -public interface IRouteCache -{ - Task InitializeAsync(); - Task ReloadAsync(); - RouteInfo? GetRoute(string tenantCode, string serviceName); - RouteInfo? GetRouteByPath(string path); -} - -// Redis 连接管理接口 -public interface IRedisConnectionManager -{ - IConnectionMultiplexer GetConnection(); - Task AcquireLockAsync(string key, TimeSpan? expiry = null); - Task ExecuteInLockAsync(string key, Func> func, TimeSpan? expiry = null); -} - -// 负载均衡策略接口 (YARP) -public interface ILoadBalancingPolicy -{ - string Name { get; } - DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList availableDestinations); -} -``` - ---- - -## 5. 入口点分析 - -### 5.1 程序入口 (`Program.cs`) - -**文件路径**: `src/Program.cs` - -**启动流程**: - -``` -1. 创建 WebApplication Builder - └── 配置 Serilog 日志 - -2. 配置选项 - ├── JwtConfig (JWT 认证配置) - └── RedisConfig (Redis 连接配置) - -3. 注册数据库服务 - └── GatewayDbContext (PostgreSQL) - -4. 注册核心服务 (Singleton) - ├── DatabaseRouteConfigProvider - ├── DatabaseClusterConfigProvider - ├── RouteCache - ├── RedisConnectionManager - ├── DynamicProxyConfigProvider - └── DistributedWeightedRoundRobinPolicy - -5. 注册后台服务 (HostedService) - ├── PgSqlConfigChangeListener - └── KubernetesPendingSyncService - -6. 配置中间件管道 - ├── CORS - ├── JwtTransformMiddleware - └── TenantRoutingMiddleware - -7. 映射端点 - ├── /health (健康检查) - ├── /api/gateway/* (管理 API) - └── /api/* (代理路由) - -8. 初始化并运行 - └── RouteCache.InitializeAsync() -``` - -### 5.2 依赖注入关系 - -``` -Program.cs - │ - ├── Config/ - │ ├── JwtConfig (Options) - │ ├── RedisConfig (Options + Singleton) - │ ├── DatabaseRouteConfigProvider (Singleton) - │ └── DatabaseClusterConfigProvider (Singleton) - │ - ├── DynamicProxy/ - │ └── DynamicProxyConfigProvider (Singleton, IProxyConfigProvider) - │ - ├── Services/ - │ ├── RouteCache (Singleton, IRouteCache) - │ ├── RedisConnectionManager (Singleton) - │ ├── PgSqlConfigChangeListener (HostedService) - │ └── KubernetesPendingSyncService (HostedService) - │ - ├── LoadBalancing/ - │ └── DistributedWeightedRoundRobinPolicy (Singleton, ILoadBalancingPolicy) - │ - └── Data/ - └── GatewayDbContext (DbContextFactory) -``` - ---- - -## 6. 技术栈 - -| 组件 | 技术 | 用途 | -|------|------|------| -| 反向代理 | YARP 2.x | 核心代理功能 | -| 数据库 | PostgreSQL + EF Core | 配置存储 | -| 缓存 | Redis | 分布式状态、锁 | -| 服务发现 | Fengling.ServiceDiscovery | Kubernetes 集成 | -| 日志 | Serilog | 结构化日志 | -| 容器化 | Docker | 部署支持 | -| 目标框架 | .NET 10.0 | 运行时 | - ---- - -## 7. 扩展点 - -1. **负载均衡策略**: 实现 `ILoadBalancingPolicy` 接口 -2. **配置提供者**: 继承 `IProxyConfigProvider` -3. **中间件**: 添加自定义中间件到管道 -4. **服务发现**: 扩展 `IServiceDiscoveryProvider` -5. **健康检查**: 配置 `HealthCheckConfig` \ No newline at end of file diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md deleted file mode 100644 index ea1b4fe..0000000 --- a/.planning/codebase/CONCERNS.md +++ /dev/null @@ -1,499 +0,0 @@ -# YARP 网关项目技术债务与关注点分析 - -> 分析日期:2026-02-28 -> 分析范围:核心代码、配置、数据访问层 - ---- - -## 一、严重安全问题 🔴 - -### 1.1 硬编码凭据泄露 - -**文件位置:** `src/Config/RedisConfig.cs:5` -```csharp -public string ConnectionString { get; set; } = "81.68.223.70:16379,password=sl52788542"; -``` - -**问题描述:** Redis 连接字符串包含明文密码,直接硬编码在源代码中。此代码提交到版本控制系统后,密码将永久暴露。 - -**影响范围:** -- 攻击者获取代码后可直接访问 Redis 服务 -- 违反安全合规要求(如等保、GDPR) - -**改进建议:** -```csharp -// 使用环境变量或密钥管理服务 -public string ConnectionString { get; set; } = - Environment.GetEnvironmentVariable("REDIS_CONNECTION_STRING") ?? string.Empty; -``` - ---- - -### 1.2 配置文件凭据泄露 - -**文件位置:** `src/appsettings.json:19,28` -```json -"DefaultConnection": "Host=81.68.223.70;Port=15432;Database=fengling_gateway;Username=movingsam;Password=sl52788542" -"ConnectionString": "81.68.223.70:6379" -``` - -**问题描述:** 数据库连接字符串和 Redis 配置包含明文凭据,且这些配置文件通常会被提交到 Git 仓库。 - -**改进建议:** -- 使用 `appsettings.Development.json` 存储开发环境配置,并加入 `.gitignore` -- 生产环境使用环境变量或 Azure Key Vault / AWS Secrets Manager -- 敏感配置使用 `dotnet user-secrets` 管理 - ---- - -### 1.3 JWT 令牌未验证 - -**文件位置:** `src/Middleware/JwtTransformMiddleware.cs:39-40` -```csharp -var jwtHandler = new JwtSecurityTokenHandler(); -var jwtToken = jwtHandler.ReadJwtToken(token); -``` - -**问题描述:** 中间件仅**读取**JWT令牌,未进行签名验证、过期检查或颁发者验证。攻击者可伪造任意JWT令牌。 - -**影响范围:** -- 任何人可伪造租户ID、用户ID、角色信息 -- 可冒充任意用户访问系统 - -**改进建议:** -```csharp -// 应使用标准的 JWT 验证流程 -var validationParameters = new TokenValidationParameters -{ - ValidateIssuer = true, - ValidateAudience = true, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ValidIssuer = _jwtConfig.Authority, - ValidAudience = _jwtConfig.Audience, - IssuerSigningKey = GetSigningKey() // 从配置获取公钥 -}; - -var principal = jwtHandler.ValidateToken(token, validationParameters, out _); -``` - ---- - -### 1.4 API 端点无认证保护 - -**文件位置:** `src/Controllers/GatewayConfigController.cs` 和 `src/Controllers/PendingServicesController.cs` - -**问题描述:** 所有管理API端点均未添加 `[Authorize]` 特性,任何人可直接调用: -- `POST /api/gateway/tenants` - 创建租户 -- `POST /api/gateway/routes` - 创建路由 -- `POST /api/gateway/clusters/{clusterId}/instances` - 添加服务实例 -- `POST /api/gateway/pending-services/{id}/assign` - 分配服务 - -**影响范围:** -- 攻击者可随意修改网关配置 -- 可注入恶意服务地址进行流量劫持 - -**改进建议:** -```csharp -[ApiController] -[Route("api/gateway")] -[Authorize(Roles = "Admin")] // 添加认证要求 -public class GatewayConfigController : ControllerBase -``` - ---- - -### 1.5 租户ID头部信任问题 - -**文件位置:** `src/Middleware/TenantRoutingMiddleware.cs:25` -```csharp -var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault(); -``` - -**问题描述:** 直接从请求头读取租户ID,未与JWT中的租户声明进行比对验证。攻击者可伪造 `X-Tenant-Id` 头部访问其他租户数据。 - -**改进建议:** -```csharp -// 从已验证的 JWT claims 中获取租户ID -var jwtTenantId = context.User.FindFirst("tenant")?.Value; -var headerTenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault(); - -if (!string.IsNullOrEmpty(jwtTenantId) && jwtTenantId != headerTenantId) -{ - // 记录安全事件 - _logger.LogWarning("Tenant ID mismatch: JWT={JwtTenant}, Header={HeaderTenant}", - jwtTenantId, headerTenantId); - context.Response.StatusCode = StatusCodes.Status403Forbidden; - return; -} -``` - ---- - -## 二、技术债务 🟠 - -### 2.1 ID生成策略问题 - -**文件位置:** `src/Controllers/GatewayConfigController.cs:484-487` -```csharp -private long GenerateId() -{ - return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); -} -``` - -**问题描述:** 使用时间戳毫秒生成ID,在高并发场景下可能产生重复ID。 - -**改进建议:** -- 使用数据库自增主键(已有配置) -- 或使用雪花算法(Snowflake ID) -- 或使用 `Guid.NewGuid()` - ---- - -### 2.2 Redis连接重复初始化 - -**文件位置:** -- `src/Program.cs:39-60` - 注册 `IConnectionMultiplexer` -- `src/Services/RedisConnectionManager.cs:25-46` - 内部再次创建连接 - -**问题描述:** Redis连接被初始化两次,造成资源浪费和配置不一致风险。 - -**改进建议:** -```csharp -// Program.cs 中只注册一次 -builder.Services.AddSingleton(); -builder.Services.AddSingleton(sp => - sp.GetRequiredService().GetConnection()); -``` - ---- - -### 2.3 DTO 内嵌定义 - -**文件位置:** `src/Controllers/GatewayConfigController.cs:444-481` - -**问题描述:** 多个DTO类定义在Controller内部,不利于复用和测试。 - -**改进建议:** -- 将 DTO 移至 `src/DTOs/` 或 `src/Models/Dto/` 目录 -- 使用 Auto Mapper 或 Mapster 进行对象映射 - ---- - -### 2.4 魔法数字 - -**文件位置:** 多处使用数字常量 - -```csharp -// RouteCache.cs:99 -.Where(r => r.Status == 1 && !r.IsDeleted) - -// GatewayConfigController.cs:239 -route.Status = 1; - -// KubernetesPendingSyncService.cs:13 -private readonly TimeSpan _syncInterval = TimeSpan.FromSeconds(30); -``` - -**问题描述:** 状态值、超时时间等使用硬编码数字,降低代码可读性和可维护性。 - -**改进建议:** -```csharp -// 定义常量或枚举 -public static class RouteStatus -{ - public const int Active = 1; - public const int Inactive = 0; -} - -public static class ServiceConstants -{ - public static readonly TimeSpan DefaultSyncInterval = TimeSpan.FromSeconds(30); -} -``` - ---- - -### 2.5 异步方法命名不一致 - -**文件位置:** `src/Config/DatabaseRouteConfigProvider.cs:23` -```csharp -_ = LoadConfigAsync(); // Fire-and-forget without await -``` - -**问题描述:** 构造函数中调用异步方法但未等待完成,可能导致初始化竞态条件。 - -**改进建议:** -- 使用工厂模式异步初始化 -- 或在 `Program.cs` 中显式调用初始化方法 - ---- - -## 三、性能瓶颈风险 🟡 - -### 3.1 负载均衡锁竞争 - -**文件位置:** `src/LoadBalancing/DistributedWeightedRoundRobinPolicy.cs:48-53` -```csharp -var lockAcquired = db.StringSet( - lockKey, - lockValue, - TimeSpan.FromMilliseconds(500), - When.NotExists -); -``` - -**问题描述:** 每次请求都需要获取Redis分布式锁,高并发下会成为瓶颈。锁获取失败时降级策略不可靠。 - -**影响:** -- 单集群QPS受限 -- Redis延迟增加时网关吞吐量下降 - -**改进建议:** -- 考虑使用本地缓存 + 定期同步策略 -- 或使用一致性哈希算法避免锁需求 -- 增加本地计数器作为快速路径 - ---- - -### 3.2 路由缓存全量加载 - -**文件位置:** `src/Services/RouteCache.cs:94-137` -```csharp -var routes = await db.TenantRoutes - .Where(r => r.Status == 1 && !r.IsDeleted) - .ToListAsync(); -``` - -**问题描述:** 每次重载都清空并重新加载所有路由,大数据量下性能差。 - -**改进建议:** -- 实现增量更新机制 -- 使用版本号比对只更新变更项 -- 添加分页加载支持 - ---- - -### 3.3 数据库查询未优化 - -**文件位置:** `src/Controllers/GatewayConfigController.cs:145-148` -```csharp -var currentRouteVersion = await db.TenantRoutes - .OrderByDescending(r => r.Version) - .Select(r => r.Version) - .FirstOrDefaultAsync(stoppingToken); -``` - -**问题描述:** 每次轮询都执行 `ORDER BY` 查询获取最大版本号,缺少索引优化。 - -**改进建议:** -```sql --- 添加索引 -CREATE INDEX IX_TenantRoutes_Version ON "TenantRoutes" ("Version" DESC); - --- 或使用 MAX 聚合 -SELECT MAX("Version") FROM "TenantRoutes"; -``` - ---- - -### 3.4 PostgreSQL NOTIFY 连接管理 - -**文件位置:** `src/Data/GatewayDbContext.cs:72-75` -```csharp -using var connection = new NpgsqlConnection(connectionString); -connection.Open(); -using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection); -cmd.ExecuteNonQuery(); -``` - -**问题描述:** 每次保存变更都创建新的数据库连接发送通知,连接开销大。 - -**改进建议:** -- 使用连接池中的连接 -- 或复用 `PgSqlConfigChangeListener` 中的连接发送通知 - ---- - -## 四、脆弱区域 🟠 - -### 4.1 租户路由外键约束 - -**文件位置:** `src/Migrations/20260201120312_InitialCreate.cs:83-89` -```csharp -table.ForeignKey( - name: "FK_TenantRoutes_Tenants_TenantCode", - column: x => x.TenantCode, - principalTable: "Tenants", - principalColumn: "TenantCode", - onDelete: ReferentialAction.Restrict); -``` - -**问题描述:** `TenantRoutes.TenantCode` 有外键约束,但全局路由(`IsGlobal=true`)时 `TenantCode` 可为空字符串,可能导致数据一致性问题。 - -**改进建议:** -- 全局路由使用特定的占位符(如 "GLOBAL") -- 或修改外键约束为条件约束 - ---- - -### 4.2 健康检查配置硬编码 - -**文件位置:** `src/Config/DatabaseClusterConfigProvider.cs:77-86` -```csharp -HealthCheck = new HealthCheckConfig -{ - Active = new ActiveHealthCheckConfig - { - Enabled = true, - Interval = TimeSpan.FromSeconds(30), - Timeout = TimeSpan.FromSeconds(5), - Path = "/health" - } -} -``` - -**问题描述:** 健康检查路径和间隔硬编码,不同服务可能需要不同的健康检查配置。 - -**改进建议:** -- 将健康检查配置存储在数据库 -- 或在模型中添加健康检查配置字段 - ---- - -### 4.3 端口选择逻辑 - -**文件位置:** `src/Controllers/PendingServicesController.cs:119-120` -```csharp -var discoveredPorts = JsonSerializer.Deserialize>(pendingService.DiscoveredPorts) ?? new List(); -var primaryPort = discoveredPorts.FirstOrDefault() > 0 ? discoveredPorts.First() : 80; -``` - -**问题描述:** 简单选择第一个端口作为主端口,可能不适合所有服务场景。 - -**改进建议:** -- 支持端口选择策略配置 -- 优先选择知名端口(如 80, 443, 8080) -- 允许用户在审批时选择端口 - ---- - -### 4.4 异常处理不完整 - -**文件位置:** `src/Services/PgSqlConfigChangeListener.cs:59-62` -```csharp -catch (Exception ex) -{ - _logger.LogError(ex, "Failed to initialize PgSql listener"); - // 未重试或终止服务 -} -``` - -**问题描述:** 初始化失败后仅记录日志,服务继续运行但功能不完整。 - -**改进建议:** -```csharp -catch (Exception ex) -{ - _logger.LogError(ex, "Failed to initialize PgSql listener, retrying in 5 seconds..."); - await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); - await InitializeListenerAsync(stoppingToken); // 重试 -} -``` - ---- - -### 4.5 状态变更无事务保护 - -**文件位置:** `src/Controllers/PendingServicesController.cs:137-145` -```csharp -db.ServiceInstances.Add(newInstance); -pendingService.Status = (int)PendingServiceStatus.Approved; -// ... -await db.SaveChangesAsync(); -``` - -**问题描述:** 创建实例和更新状态在同一事务中,但如果缓存重载失败,数据可能不一致。 - -**改进建议:** -- 使用 TransactionScope 或数据库事务明确边界 -- 添加补偿机制处理失败情况 - ---- - -## 五、可维护性问题 🟡 - -### 5.1 日志结构不统一 - -**问题描述:** 日志消息格式不统一,有的包含结构化数据,有的仅是文本。 - -**改进建议:** -- 制定统一的日志格式规范 -- 使用结构化日志模板:`LogInformation("Operation {Operation} completed for {Entity} with ID {Id}", "Create", "Route", route.Id)` - ---- - -### 5.2 缺少单元测试 - -**问题描述:** 项目中未发现测试项目,核心逻辑缺少测试覆盖。 - -**改进建议:** -- 创建 `tests/YarpGateway.Tests/` 测试项目 -- 对以下核心组件编写单元测试: - - `RouteCache` - 路由查找逻辑 - - `JwtTransformMiddleware` - JWT 解析逻辑 - - `DistributedWeightedRoundRobinPolicy` - 负载均衡算法 - ---- - -### 5.3 配置验证缺失 - -**文件位置:** `src/Config/JwtConfig.cs`, `src/Config/RedisConfig.cs` - -**问题描述:** 配置类没有验证逻辑,无效配置可能导致运行时错误。 - -**改进建议:** -```csharp -public class JwtConfig : IValidatableObject -{ - public string Authority { get; set; } = string.Empty; - - public IEnumerable Validate(ValidationContext validationContext) - { - if (string.IsNullOrWhiteSpace(Authority)) - yield return new ValidationResult("Authority is required", new[] { nameof(Authority) }); - } -} -``` - ---- - -## 六、改进优先级建议 - -| 优先级 | 问题 | 风险等级 | 建议处理时间 | -|--------|------|----------|--------------| -| P0 | 硬编码凭据泄露 | 严重 | 立即修复 | -| P0 | JWT未验证 | 严重 | 立即修复 | -| P0 | API无认证保护 | 严重 | 立即修复 | -| P1 | 租户ID信任问题 | 高 | 1周内 | -| P1 | ID生成策略 | 高 | 1周内 | -| P2 | 负载均衡锁竞争 | 中 | 2周内 | -| P2 | 路由缓存优化 | 中 | 2周内 | -| P3 | DTO内嵌定义 | 低 | 1个月内 | -| P3 | 缺少单元测试 | 低 | 持续改进 | - ---- - -## 七、总结 - -本项目存在多个**严重安全漏洞**,主要涉及: -1. 敏感信息硬编码 -2. 认证授权缺失 -3. 输入验证不足 - -技术债务主要集中在代码组织、异常处理和性能优化方面。建议优先处理安全相关问题,然后逐步优化性能和可维护性。 - ---- - -*文档由自动化分析生成,建议人工复核后纳入迭代计划。* \ No newline at end of file diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md deleted file mode 100644 index 6661e0f..0000000 --- a/.planning/codebase/CONVENTIONS.md +++ /dev/null @@ -1,690 +0,0 @@ -# YARP Gateway 编码约定文档 - -## 概述 - -本文档记录了 YARP Gateway 项目的编码约定和最佳实践,旨在帮助开发人员理解和遵循项目规范。 - ---- - -## 1. 代码风格 - -### 1.1 命名约定 - -#### 类和接口命名 - -```csharp -// 接口:使用 I 前缀 + PascalCase -public interface IRouteCache -{ - Task InitializeAsync(); - Task ReloadAsync(); - RouteInfo? GetRoute(string tenantCode, string serviceName); -} - -// 实现类:PascalCase,描述性名称 -public class RouteCache : IRouteCache -{ - // ... -} - -// 配置类:以 Config 后缀 -public class RedisConfig -{ - public string ConnectionString { get; set; } = "81.68.223.70:16379,password=sl52788542"; - public int Database { get; set; } = 0; - public string InstanceName { get; set; } = "YarpGateway"; -} - -// DTO 类:以 Dto 后缀 -public class CreateTenantDto -{ - public string TenantCode { get; set; } = string.Empty; - public string TenantName { get; set; } = string.Empty; -} - -// 数据模型:Gw 前缀标识网关实体 -public class GwTenantRoute -{ - public long Id { get; set; } - public string TenantCode { get; set; } = string.Empty; - // ... -} -``` - -#### 私有字段命名 - -```csharp -// 使用下划线前缀 + camelCase -public class TenantRoutingMiddleware -{ - private readonly RequestDelegate _next; - private readonly IRouteCache _routeCache; - private readonly ILogger _logger; -} -``` - -**原因**:下划线前缀清晰区分私有字段和局部变量,避免 `this.` 的频繁使用。 - -#### 方法命名 - -```csharp -// 异步方法:Async 后缀 -public async Task InitializeAsync() -public async Task ReloadAsync() -private async Task LoadFromDatabaseAsync() - -// 同步方法:动词开头 -public RouteInfo? GetRoute(string tenantCode, string serviceName) -private string ExtractServiceName(string path) -``` - -### 1.2 文件组织 - -项目采用按功能分层的方式组织代码: - -``` -src/ -├── Config/ # 配置类和配置提供者 -├── Controllers/ # API 控制器 -├── Data/ # 数据库上下文和工厂 -├── DynamicProxy/ # 动态代理配置 -├── LoadBalancing/ # 负载均衡策略 -├── Metrics/ # 指标收集 -├── Middleware/ # 中间件 -├── Migrations/ # 数据库迁移 -├── Models/ # 数据模型 -└── Services/ # 业务服务 -``` - -**原因**:按功能分层便于代码定位,降低耦合度。 - ---- - -## 2. 依赖注入模式 - -### 2.1 服务注册 - -```csharp -// Program.cs 中的服务注册 - -// 配置选项模式 -builder.Services.Configure(builder.Configuration.GetSection("Jwt")); -builder.Services.Configure(builder.Configuration.GetSection("Redis")); - -// 直接注册配置实例(当需要直接使用配置对象时) -builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); - -// DbContext 使用工厂模式 -builder.Services.AddDbContextFactory(options => - options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")) -); - -// 单例服务(无状态或线程安全) -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -// 接口与实现分离注册 -builder.Services.AddSingleton(); - -// 后台服务 -builder.Services.AddHostedService(); -builder.Services.AddHostedService(); -``` - -### 2.2 依赖注入构造函数模式 - -```csharp -public class RouteCache : IRouteCache -{ - private readonly IDbContextFactory _dbContextFactory; - private readonly ILogger _logger; - - public RouteCache( - IDbContextFactory dbContextFactory, - ILogger logger) - { - _dbContextFactory = dbContextFactory; - _logger = logger; - } -} -``` - -**模式要点**: -1. 所有依赖通过构造函数注入 -2. 使用 `readonly` 修饰私有字段 -3. 依赖项按类别排序(框架 → 基础设施 → 业务服务) - -**原因**:构造函数注入确保依赖不可变,便于测试和依赖管理。 - -### 2.3 IDbContextFactory 模式 - -```csharp -// 在 Singleton 服务中使用 DbContextFactory -public class RouteCache : IRouteCache -{ - private readonly IDbContextFactory _dbContextFactory; - - private async Task LoadFromDatabaseAsync() - { - // 使用 using 确保上下文正确释放 - using var db = _dbContextFactory.CreateDbContext(); - var routes = await db.TenantRoutes - .Where(r => r.Status == 1 && !r.IsDeleted) - .ToListAsync(); - // ... - } -} - -// 在 BackgroundService 中使用 Scope -public class KubernetesPendingSyncService : BackgroundService -{ - private readonly IServiceProvider _serviceProvider; - - private async Task SyncPendingServicesAsync(CancellationToken ct) - { - // 创建作用域以获取 Scoped 服务 - using var scope = _serviceProvider.CreateScope(); - var dbContextFactory = scope.ServiceProvider.GetRequiredService>(); - // ... - } -} -``` - -**原因**:`IDbContextFactory` 避免了 Singleton 服务直接持有 DbContext 的生命周期问题。 - ---- - -## 3. 配置管理模式 - -### 3.1 配置类定义 - -```csharp -// 简单 POCO 配置类 -namespace YarpGateway.Config; - -public class JwtConfig -{ - public string Authority { get; set; } = string.Empty; - public string Audience { get; set; } = string.Empty; - public bool ValidateIssuer { get; set; } = true; - public bool ValidateAudience { get; set; } = true; -} -``` - -### 3.2 配置绑定和注入 - -```csharp -// Program.cs 中绑定配置 -builder.Services.Configure(builder.Configuration.GetSection("Jwt")); - -// 通过 IOptions 注入 -public class JwtTransformMiddleware -{ - private readonly JwtConfig _jwtConfig; - - public JwtTransformMiddleware( - RequestDelegate next, - IOptions jwtConfig, // 使用 IOptions - ILogger logger) - { - _jwtConfig = jwtConfig.Value; // 获取实际配置值 - _logger = logger; - } -} -``` - -### 3.3 动态配置更新 - -```csharp -// 配置变更通知通道 -public static class ConfigNotifyChannel -{ - public const string GatewayConfigChanged = "gateway_config_changed"; -} - -// DbContext 在保存时检测变更并通知 -public class GatewayDbContext : DbContext -{ - private bool _configChangeDetected; - - public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) - { - DetectConfigChanges(); - var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); - if (_configChangeDetected) - { - await NotifyConfigChangedAsync(cancellationToken); - } - return result; - } - - private void DetectConfigChanges() - { - var entries = ChangeTracker.Entries() - .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) - .Where(e => e.Entity is GwTenantRoute or GwServiceInstance or GwTenant); - _configChangeDetected = entries.Any(); - } -} -``` - -**原因**:使用 PostgreSQL NOTIFY/LISTEN 实现配置热更新,避免轮询。 - ---- - -## 4. 错误处理方式 - -### 4.1 中间件错误处理 - -```csharp -public class JwtTransformMiddleware -{ - public async Task InvokeAsync(HttpContext context) - { - // 快速失败模式:前置条件检查后直接调用 next - var authHeader = context.Request.Headers["Authorization"].FirstOrDefault(); - if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) - { - await _next(context); - return; - } - - try - { - // 业务逻辑 - var jwtHandler = new JwtSecurityTokenHandler(); - var jwtToken = jwtHandler.ReadJwtToken(token); - // ... - } - catch (Exception ex) - { - // 记录错误但不中断请求流程 - _logger.LogError(ex, "Failed to parse JWT token"); - } - - await _next(context); - } -} -``` - -### 4.2 控制器错误处理 - -```csharp -[HttpPost("{id}/assign")] -public async Task AssignService(long id, [FromBody] AssignServiceRequest request) -{ - await using var db = _dbContextFactory.CreateDbContext(); - - // 早期返回模式 - var pendingService = await db.PendingServiceDiscoveries.FindAsync(id); - if (pendingService == null || pendingService.IsDeleted) - { - return NotFound(new { message = "Pending service not found" }); - } - - if (pendingService.Status != (int)PendingServiceStatus.Pending) - { - return BadRequest(new { message = $"Service is already {((PendingServiceStatus)pendingService.Status)}, cannot assign" }); - } - - if (string.IsNullOrEmpty(request.ClusterId)) - { - return BadRequest(new { message = "ClusterId is required" }); - } - - // 业务逻辑... - return Ok(new { success = true, message = "..." }); -} -``` - -**模式要点**: -1. 使用早期返回(Guard Clauses)减少嵌套 -2. 返回结构化的错误信息 -3. 使用 HTTP 状态码语义 - -### 4.3 后台服务错误处理 - -```csharp -protected override async Task ExecuteAsync(CancellationToken stoppingToken) -{ - while (!stoppingToken.IsCancellationRequested) - { - try - { - await SyncPendingServicesAsync(stoppingToken); - } - catch (Exception ex) - { - // 记录错误但继续运行 - _logger.LogError(ex, "Error during K8s pending service sync"); - } - - await Task.Delay(_syncInterval, stoppingToken); - } -} -``` - -**原因**:后台服务不应因单次错误而终止,需具备自恢复能力。 - ---- - -## 5. 日志记录约定 - -### 5.1 结构化日志 - -```csharp -// 使用 Serilog 结构化日志 -_logger.LogInformation("Route cache initialized: {GlobalCount} global routes, {TenantCount} tenant routes", - _globalRoutes.Count, _tenantRoutes.Count); - -_logger.LogWarning("No route found for: {Tenant}/{Service}", tenantCode, serviceName); - -_logger.LogError(ex, "Redis connection failed"); - -_logger.LogDebug("Released lock for key: {Key}", _key); -``` - -**模式要点**: -1. 使用占位符 `{PropertyName}` 而非字符串插值 -2. 日志消息使用常量,便于聚合分析 -3. 包含足够的上下文信息 - -### 5.2 Serilog 配置 - -```csharp -// Program.cs -builder.Host.UseSerilog( - (context, services, configuration) => - configuration - .ReadFrom.Configuration(context.Configuration) - .ReadFrom.Services(services) - .Enrich.FromLogContext() -); -``` - -### 5.3 日志级别使用 - -| 级别 | 使用场景 | -|------|----------| -| `LogDebug` | 详细调试信息,生产环境通常关闭 | -| `LogInformation` | 正常业务流程关键节点 | -| `LogWarning` | 可恢复的异常情况 | -| `LogError` | 错误需要关注但不影响整体运行 | -| `LogFatal` | 致命错误,应用无法继续运行 | - ---- - -## 6. 异步编程模式 - -### 6.1 async/await 使用 - -```csharp -// 正确:异步方法使用 Async 后缀 -public async Task InitializeAsync() -{ - _logger.LogInformation("Initializing route cache from database..."); - await LoadFromDatabaseAsync(); -} - -// 正确:使用 ConfigureAwait(false) 在库代码中 -private async Task LoadFromDatabaseAsync() -{ - using var db = _dbContextFactory.CreateDbContext(); - var routes = await db.TenantRoutes - .Where(r => r.Status == 1 && !r.IsDeleted) - .ToListAsync(); - // ... -} -``` - -### 6.2 CancellationToken 使用 - -```csharp -// 控制器方法 -[HttpGet] -public async Task GetPendingServices( - [FromQuery] int page = 1, - [FromQuery] int pageSize = 10, - [FromQuery] int? status = null) -{ - await using var db = _dbContextFactory.CreateDbContext(); - // EF Core 自动处理 CancellationToken - var total = await query.CountAsync(); - // ... -} - -// 后台服务 -protected override async Task ExecuteAsync(CancellationToken stoppingToken) -{ - while (!stoppingToken.IsCancellationRequested) - { - await Task.Delay(_syncInterval, stoppingToken); - } -} -``` - -### 6.3 并发控制 - -```csharp -// 使用 ReaderWriterLockSlim 保护读写 -public class RouteCache : IRouteCache -{ - private readonly ReaderWriterLockSlim _lock = new(); - - public RouteInfo? GetRoute(string tenantCode, string serviceName) - { - _lock.EnterUpgradeableReadLock(); - try - { - // 读取逻辑 - } - finally - { - _lock.ExitUpgradeableReadLock(); - } - } - - private async Task LoadFromDatabaseAsync() - { - _lock.EnterWriteLock(); - try - { - // 写入逻辑 - } - finally - { - _lock.ExitWriteLock(); - } - } -} - -// 使用 SemaphoreSlim 进行异步锁定 -public class DatabaseRouteConfigProvider -{ - private readonly SemaphoreSlim _lock = new(1, 1); - - public async Task ReloadAsync() - { - await _lock.WaitAsync(); - try - { - await LoadConfigInternalAsync(); - } - finally - { - _lock.Release(); - } - } -} -``` - -**原因**: -- `ReaderWriterLockSlim` 支持多读单写,适合读多写少场景 -- `SemaphoreSlim` 支持异步等待,适合异步方法 - -### 6.4 Redis 分布式锁模式 - -```csharp -public async Task AcquireLockAsync(string key, TimeSpan? expiry = null) -{ - var redis = GetConnection(); - var db = redis.GetDatabase(); - var lockKey = $"lock:{_config.InstanceName}:{key}"; - var lockValue = Environment.MachineName + ":" + Process.GetCurrentProcess().Id; - - var acquired = await db.StringSetAsync(lockKey, lockValue, expiryTime, When.NotExists); - - if (!acquired) - { - // 退避重试 - var backoff = TimeSpan.FromMilliseconds(100); - while (!acquired && retryCount < maxRetries) - { - await Task.Delay(backoff); - acquired = await db.StringSetAsync(lockKey, lockValue, expiryTime, When.NotExists); - retryCount++; - } - } - - return new RedisLock(db, lockKey, lockValue, _logger); -} -``` - ---- - -## 7. 中间件模式 - -### 7.1 标准中间件结构 - -```csharp -public class TenantRoutingMiddleware -{ - private readonly RequestDelegate _next; - private readonly IRouteCache _routeCache; - private readonly ILogger _logger; - - // 构造函数注入依赖 - public TenantRoutingMiddleware( - RequestDelegate next, - IRouteCache routeCache, - ILogger logger) - { - _next = next; - _routeCache = routeCache; - _logger = logger; - } - - // InvokeAsync 方法签名固定 - public async Task InvokeAsync(HttpContext context) - { - // 1. 前置处理 - var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault(); - - // 2. 快速返回 - if (string.IsNullOrEmpty(tenantId)) - { - await _next(context); - return; - } - - // 3. 业务逻辑 - var route = _routeCache.GetRoute(tenantId, serviceName); - - // 4. 设置上下文 - context.Items["DynamicClusterId"] = route.ClusterId; - - // 5. 调用下一个中间件 - await _next(context); - } -} -``` - -### 7.2 中间件注册顺序 - -```csharp -// Program.cs -var app = builder.Build(); - -app.UseCors("AllowFrontend"); -app.UseMiddleware(); // JWT 解析 -app.UseMiddleware(); // 租户路由 - -app.MapControllers(); -app.MapReverseProxy(); -``` - -**顺序原因**: -1. CORS 需最先处理跨域请求 -2. JWT 中间件解析用户信息供后续使用 -3. 租户路由根据用户信息选择目标服务 - ---- - -## 8. 控制器约定 - -### 8.1 控制器结构 - -```csharp -[ApiController] -[Route("api/gateway")] -public class GatewayConfigController : ControllerBase -{ - private readonly IDbContextFactory _dbContextFactory; - private readonly IRouteCache _routeCache; - - public GatewayConfigController( - IDbContextFactory dbContextFactory, - IRouteCache routeCache) - { - _dbContextFactory = dbContextFactory; - _routeCache = routeCache; - } - - #region Tenants - // 租户相关端点 - #endregion - - #region Routes - // 路由相关端点 - #endregion -} -``` - -### 8.2 端点命名 - -```csharp -// GET 集合 -[HttpGet("tenants")] -public async Task GetTenants(...) { } - -// GET 单个 -[HttpGet("tenants/{id}")] -public async Task GetTenant(long id) { } - -// POST 创建 -[HttpPost("tenants")] -public async Task CreateTenant([FromBody] CreateTenantDto dto) { } - -// PUT 更新 -[HttpPut("tenants/{id}")] -public async Task UpdateTenant(long id, [FromBody] UpdateTenantDto dto) { } - -// DELETE 删除 -[HttpDelete("tenants/{id}")] -public async Task DeleteTenant(long id) { } -``` - ---- - -## 9. 总结 - -本项目的编码约定遵循以下核心原则: - -1. **一致性**:统一的命名和代码组织方式 -2. **可测试性**:依赖注入和接口抽象便于测试 -3. **可维护性**:清晰的结构和文档注释 -4. **可观测性**:结构化日志和指标收集 -5. **健壮性**:完善的错误处理和并发控制 - -遵循这些约定可以确保代码质量和团队协作效率。 \ No newline at end of file diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md deleted file mode 100644 index 0d6d558..0000000 --- a/.planning/codebase/INTEGRATIONS.md +++ /dev/null @@ -1,374 +0,0 @@ -# YARP 网关外部集成文档 - -## 1. PostgreSQL 数据库集成 - -### 概述 -PostgreSQL 作为主数据库,存储网关配置数据,包括租户、路由、服务实例等信息。 - -### 连接配置 -**配置位置**: `src/appsettings.json` - -```json -{ - "ConnectionStrings": { - "DefaultConnection": "Host=81.68.223.70;Port=15432;Database=fengling_gateway;Username=movingsam;Password=***" - } -} -``` - -### DbContext 配置 -**文件**: `src/Data/GatewayDbContext.cs` - -```csharp -// 注册 DbContext 工厂 -builder.Services.AddDbContextFactory(options => - options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")) -); -``` - -### 数据模型 -| 实体 | 表名 | 用途 | -|------|------|------| -| `GwTenant` | Tenants | 租户信息 | -| `GwTenantRoute` | TenantRoutes | 租户路由配置 | -| `GwServiceInstance` | ServiceInstances | 服务实例(集群节点) | -| `GwPendingServiceDiscovery` | PendingServiceDiscoveries | K8s 待处理服务发现 | - -### 配置变更通知机制 -**文件**: `src/Config/ConfigNotifyChannel.cs` - -使用 PostgreSQL `LISTEN/NOTIFY` 机制实现配置变更实时通知: - -```csharp -// 发送通知(在 DbContext.SaveChangesAsync 中触发) -await using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection); - -// 监听通知(在 PgSqlConfigChangeListener 中) -cmd.CommandText = $"LISTEN {ConfigNotifyChannel.GatewayConfigChanged}"; -``` - -**监听服务**: `src/Services/PgSqlConfigChangeListener.cs` -- 监听 PostgreSQL NOTIFY 通道 -- 检测配置版本变更 -- 触发路由/集群配置热更新 -- 提供 5 分钟兜底轮询机制 - ---- - -## 2. Redis 集成 - -### 概述 -Redis 用于分布式锁、路由缓存同步,确保多实例网关的配置一致性。 - -### 连接配置 -**配置位置**: `src/Config/RedisConfig.cs` - -```csharp -public class RedisConfig -{ - public string ConnectionString { get; set; } = "81.68.223.70:16379,password=***"; - public int Database { get; set; } = 0; - public string InstanceName { get; set; } = "YarpGateway"; -} -``` - -### 连接管理器 -**文件**: `src/Services/RedisConnectionManager.cs` - -```csharp -// 注册 Redis 连接 -builder.Services.AddSingleton(sp => -{ - var config = sp.GetRequiredService(); - var connectionOptions = ConfigurationOptions.Parse(config.ConnectionString); - connectionOptions.AbortOnConnectFail = false; - connectionOptions.ConnectRetry = 3; - connectionOptions.ConnectTimeout = 5000; - connectionOptions.SyncTimeout = 3000; - connectionOptions.DefaultDatabase = config.Database; - - return ConnectionMultiplexer.Connect(connectionOptions); -}); -``` - -### 分布式锁实现 -**接口**: `IRedisConnectionManager` - -```csharp -public interface IRedisConnectionManager -{ - IConnectionMultiplexer GetConnection(); - Task AcquireLockAsync(string key, TimeSpan? expiry = null); - Task ExecuteInLockAsync(string key, Func> func, TimeSpan? expiry = null); -} -``` - -**锁机制特性**: -- 基于键值对的分布式锁 -- 自动过期时间(默认 10 秒) -- 指数退避重试策略 -- Lua 脚本安全释放锁 - ---- - -## 3. Kubernetes 服务发现集成 - -### 概述 -通过自定义的 Fengling.ServiceDiscovery 包实现 Kubernetes 服务自动发现,将 K8s Service 自动注册为网关后端服务。 - -### 配置 -**文件**: `src/Program.cs` - -```csharp -// 添加 Kubernetes 服务发现 -var useInClusterConfig = builder.Configuration.GetValue("ServiceDiscovery:UseInClusterConfig", true); -builder.Services.AddKubernetesServiceDiscovery(options => -{ - options.LabelSelector = "app.kubernetes.io/managed-by=yarp"; - options.UseInClusterConfig = useInClusterConfig; -}); - -builder.Services.AddServiceDiscovery(); -``` - -### 依赖包 -| 包名 | 用途 | -|------|------| -| `Fengling.ServiceDiscovery.Core` | 服务发现核心接口 | -| `Fengling.ServiceDiscovery.Kubernetes` | Kubernetes 实现 | -| `Fengling.ServiceDiscovery.Static` | 静态配置实现 | - -### 后台同步服务 -**文件**: `src/Services/KubernetesPendingSyncService.cs` - -```csharp -public class KubernetesPendingSyncService : BackgroundService -{ - private readonly TimeSpan _syncInterval = TimeSpan.FromSeconds(30); - private readonly TimeSpan _staleThreshold = TimeSpan.FromHours(24); - - // 同步 K8s 服务到数据库待处理表 -} -``` - -**同步逻辑**: -1. 每 30 秒从 K8s API 获取服务列表 -2. 对比数据库中的待处理服务记录 -3. 新增/更新/清理过期服务 -4. 标记不再存在的 K8s 服务 - -### 待处理服务数据模型 -**文件**: `src/Models/GwPendingServiceDiscovery.cs` - -```csharp -public class GwPendingServiceDiscovery -{ - public string K8sServiceName { get; set; } // K8s Service 名称 - public string K8sNamespace { get; set; } // K8s 命名空间 - public string K8sClusterIP { get; set; } // ClusterIP - public string DiscoveredPorts { get; set; } // JSON 序列化的端口列表 - public string Labels { get; set; } // K8s 标签 - public string AssignedClusterId { get; set; } // 分配的集群 ID - public int Status { get; set; } // 状态 -} -``` - ---- - -## 4. JWT 认证集成 - -### 概述 -网关解析 JWT Token,提取租户和用户信息,转换为下游服务可用的 HTTP 头。 - -### 配置 -**文件**: `src/Config/JwtConfig.cs` - -```csharp -public class JwtConfig -{ - public string Authority { get; set; } = string.Empty; // 认证服务器地址 - public string Audience { get; set; } = string.Empty; // 受众 - public bool ValidateIssuer { get; set; } = true; // 验证签发者 - public bool ValidateAudience { get; set; } = true; // 验证受众 -} -``` - -**配置示例** (`src/appsettings.json`): -```json -{ - "Jwt": { - "Authority": "https://your-auth-server.com", - "Audience": "fengling-gateway", - "ValidateIssuer": true, - "ValidateAudience": true - } -} -``` - -### JWT 转换中间件 -**文件**: `src/Middleware/JwtTransformMiddleware.cs` - -```csharp -public async Task InvokeAsync(HttpContext context) -{ - var authHeader = context.Request.Headers["Authorization"].FirstOrDefault(); - if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ")) - { - var token = authHeader.Substring("Bearer ".Length).Trim(); - var jwtToken = jwtHandler.ReadJwtToken(token); - - // 提取声明并转换为 HTTP 头 - var tenantId = jwtToken.Claims.FirstOrDefault(c => c.Type == "tenant")?.Value; - var userId = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; - - context.Request.Headers["X-Tenant-Id"] = tenantId; - context.Request.Headers["X-User-Id"] = userId; - context.Request.Headers["X-User-Name"] = userName; - context.Request.Headers["X-Roles"] = string.Join(",", roles); - } - await _next(context); -} -``` - -### JWT 声明到 HTTP 头映射 -| JWT 声明类型 | HTTP 头 | 说明 | -|--------------|---------|------| -| `tenant` | `X-Tenant-Id` | 租户标识 | -| `ClaimTypes.NameIdentifier` | `X-User-Id` | 用户 ID | -| `ClaimTypes.Name` | `X-User-Name` | 用户名 | -| `ClaimTypes.Role` | `X-Roles` | 角色列表(逗号分隔) | - ---- - -## 5. 外部 API 和服务连接 - -### CORS 配置 -**文件**: `src/appsettings.json` - -```json -{ - "Cors": { - "AllowedOrigins": [ - "http://localhost:5173", - "http://127.0.0.1:5173", - "http://localhost:5174" - ], - "AllowAnyOrigin": false - } -} -``` - -### 健康检查端点 -**文件**: `src/Program.cs` - -```csharp -app.MapGet("/health", () => Results.Ok(new { - status = "healthy", - timestamp = DateTime.UtcNow -})); -``` - -### 下游服务健康检查 -**文件**: `src/Config/DatabaseClusterConfigProvider.cs` - -```csharp -HealthCheck = new HealthCheckConfig -{ - Active = new ActiveHealthCheckConfig - { - Enabled = true, - Interval = TimeSpan.FromSeconds(30), - Timeout = TimeSpan.FromSeconds(5), - Path = "/health" - } -} -``` - -### 动态代理配置 -**文件**: `src/DynamicProxy/DynamicProxyConfigProvider.cs` - -实现 `IProxyConfigProvider` 接口,从数据库动态加载路由和集群配置: - -```csharp -public class DynamicProxyConfigProvider : IProxyConfigProvider -{ - public IProxyConfig GetConfig() => _config; - - public void UpdateConfig() - { - var routes = _routeProvider.GetRoutes(); - var clusters = _clusterProvider.GetClusters(); - _config = new InMemoryProxyConfig(routes, clusters, ...); - } -} -``` - ---- - -## 6. 集成架构图 - -``` - ┌─────────────────────────────────────────────────────────────┐ - │ 客户端请求 │ - └─────────────────────────────────────────────────────────────┘ - │ - ▼ - ┌─────────────────────────────────────────────────────────────┐ - │ YARP Gateway │ - │ ┌─────────────────────────────────────────────────────┐ │ - │ │ 中间件管道 │ │ - │ │ CORS → JWT转换 → 租户路由 → Controllers → Proxy │ │ - │ └─────────────────────────────────────────────────────┘ │ - │ │ - │ ┌──────────┐ ┌──────────┐ ┌──────────────────────────┐ │ - │ │RouteCache│ │ConfigProv│ │LoadBalancingPolicy │ │ - │ └────┬─────┘ └────┬─────┘ └──────────────────────────┘ │ - └───────┼─────────────┼────────────────────────────────────────┘ - │ │ - ┌───────────────┼─────────────┼───────────────┐ - │ │ │ │ - ▼ ▼ ▼ ▼ - ┌───────────────┐ ┌───────────┐ ┌───────────┐ ┌───────────────────┐ - │ PostgreSQL │ │ Redis │ │ K8s │ │ Auth Server │ - │ │ │ │ │ API │ │ (JWT) │ - │ - 租户配置 │ │ - 分布式锁│ │ │ │ │ - │ - 路由配置 │ │ - 缓存 │ │ - Service │ │ - Token 签发 │ - │ - 服务实例 │ │ │ │ - Pod │ │ - 声明信息 │ - │ - NOTIFY机制 │ │ │ │ │ │ │ - └───────────────┘ └───────────┘ └───────────┘ └───────────────────┘ - │ - │ LISTEN/NOTIFY - ▼ - ┌───────────────────────────────────────────────────────┐ - │ 配置变更监听器 │ - │ PgSqlConfigChangeListener + FallbackPolling │ - └───────────────────────────────────────────────────────┘ -``` - ---- - -## 7. 配置热更新流程 - -``` -数据库配置变更 - │ - ▼ -DbContext.SaveChangesAsync() - │ - ▼ -NOTIFY gateway_config_changed - │ - ▼ -PgSqlConfigChangeListener.OnNotification() - │ - ▼ -RouteCache.ReloadAsync() - │ - ▼ -DynamicProxyConfigProvider.UpdateConfig() - │ - ▼ -YARP 配置生效(无需重启) -``` - -**兜底机制**: 每 5 分钟检查版本号,防止 NOTIFY 丢失导致配置不一致。 \ No newline at end of file diff --git a/.planning/codebase/SECURITY_AUDIT.md b/.planning/codebase/SECURITY_AUDIT.md deleted file mode 100644 index a2db93b..0000000 --- a/.planning/codebase/SECURITY_AUDIT.md +++ /dev/null @@ -1,368 +0,0 @@ -# 🔒 YARP 网关安全审计报告 - -> 审计日期:2026-02-28 -> 审计范围:认证授权、注入漏洞、敏感信息、访问控制、配置安全 - ---- - -## 执行摘要 - -| 严重程度 | 数量 | -|---------|------| -| 🔴 严重 (CRITICAL) | 3 | -| 🟠 高危 (HIGH) | 3 | -| 🟡 中危 (MEDIUM) | 4 | -| 🟢 低危 (LOW) | 3 | -| **总计** | **13** | - ---- - -## 🔴 严重漏洞 - -### 1. 硬编码数据库凭据泄露 - -**文件:** `src/appsettings.json` 第 19 行 - -**问题代码:** -```json -"DefaultConnection": "Host=81.68.223.70;Port=15432;Database=fengling_gateway;Username=movingsam;Password=sl52788542" -``` - -**攻击场景:** -- 代码泄露或被推送到公开仓库时,攻击者直接获得数据库完整访问权限 -- 可读取、修改、删除所有业务数据 - -**修复建议:** -```csharp -// 使用环境变量或 Secret Manager -var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); -// 或使用 Azure Key Vault / AWS Secrets Manager -``` - ---- - -### 2. 硬编码 Redis 凭据泄露 - -**文件:** `src/Config/RedisConfig.cs` 第 5 行 - -**问题代码:** -```csharp -public string ConnectionString { get; set; } = "81.68.223.70:16379,password=sl52788542"; -``` - -**攻击场景:** -- 攻击者可连接 Redis 服务器,读取缓存数据、修改路由配置、注入恶意数据 - -**修复建议:** -```csharp -public string ConnectionString { get; set; } = string.Empty; -// 从环境变量或配置中心读取 -``` - ---- - -### 3. 管理 API 完全无认证保护 - -**文件:** `src/Controllers/GatewayConfigController.cs` 及 `src/Controllers/PendingServicesController.cs` - -**问题描述:** -- 所有 API 端点均无 `[Authorize]` 特性 -- `Program.cs` 中未配置 `AddAuthentication()` 和 `UseAuthentication()` -- 项目搜索未发现任何认证中间件 - -**攻击场景:** -``` -# 攻击者可直接调用以下 API: -POST /api/gateway/tenants # 创建任意租户 -DELETE /api/gateway/tenants/{id} # 删除租户 -POST /api/gateway/routes # 创建恶意路由 -POST /api/gateway/config/reload # 重载配置 -DELETE /api/gateway/clusters/{id} # 删除服务集群 -``` - -**修复建议:** -```csharp -// Program.cs -builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => { /* 配置 JWT 验证 */ }); - -builder.Services.AddAuthorization(); - -app.UseAuthentication(); -app.UseAuthorization(); - -// Controllers -[ApiController] -[Route("api/gateway")] -[Authorize] // 添加认证要求 -public class GatewayConfigController : ControllerBase -``` - ---- - -## 🟠 高危漏洞 - -### 4. JWT 签名验证缺失 - -**文件:** `src/Middleware/JwtTransformMiddleware.cs` 第 39-40 行 - -**问题代码:** -```csharp -var jwtHandler = new JwtSecurityTokenHandler(); -var jwtToken = jwtHandler.ReadJwtToken(token); // 仅读取,不验证! -``` - -**攻击场景:** -```python -# 攻击者可伪造任意 JWT -import jwt -fake_token = jwt.encode({"tenant": "admin-tenant", "sub": "admin"}, "any_secret", algorithm="HS256") -# 网关会接受这个伪造的 token -``` - -**修复建议:** -```csharp -var validationParameters = new TokenValidationParameters -{ - ValidateIssuer = true, - ValidateAudience = true, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ValidIssuer = _jwtConfig.Authority, - ValidAudience = _jwtConfig.Audience, - IssuerSigningKey = /* 从 Authority 获取公钥 */ -}; - -var principal = jwtHandler.ValidateToken(token, validationParameters, out _); -``` - ---- - -### 5. 租户隔离可被 Header 注入绕过 - -**文件:** `src/Middleware/JwtTransformMiddleware.cs` 第 54 行 - -**问题代码:** -```csharp -context.Request.Headers["X-Tenant-Id"] = tenantId; -``` - -**攻击场景:** -```bash -# 攻击者直接注入 Header 绕过 JWT -curl -H "X-Tenant-Id: target-tenant" \ - -H "X-User-Id: admin" \ - -H "X-Roles: admin" \ - https://gateway/api/sensitive-data -``` - -**修复建议:** -```csharp -// 在中间件开始时移除所有 X-* Header -foreach (var header in context.Request.Headers.Where(h => h.Key.StartsWith("X-")).ToList()) -{ - context.Request.Headers.Remove(header.Key); -} - -// 然后再从 JWT 设置可信的 header -``` - ---- - -### 6. 租户路由信息泄露 - -**文件:** `src/Middleware/TenantRoutingMiddleware.cs` 第 44 行 - -**问题代码:** -```csharp -_logger.LogWarning("Route not found - Tenant: {Tenant}, Service: {Service}", tenantId, serviceName); -``` - -**攻击场景:** -- 日志中记录租户 ID 和服务名,攻击者可通过日志收集系统架构信息 -- 配合其他攻击进行侦察 - -**修复建议:** -- 敏感信息不应记录到普通日志 -- 使用脱敏处理或仅记录哈希值 - ---- - -## 🟡 中危漏洞 - -### 7. 日志记录敏感连接信息 - -**文件:** `src/Services/RedisConnectionManager.cs` 第 44 行 - -**问题代码:** -```csharp -_logger.LogInformation("Connected to Redis at {ConnectionString}", _config.ConnectionString); -``` - -**修复建议:** -```csharp -_logger.LogInformation("Connected to Redis at {Host}", - configuration.EndPoints.FirstOrDefault()?.ToString()); -``` - ---- - -### 8. CORS 凭据配置存在风险 - -**文件:** `src/Program.cs` 第 89-100 行 - -**问题代码:** -```csharp -if (allowAnyOrigin) -{ - policy.AllowAnyOrigin(); -} -// ... -policy.AllowAnyHeader() - .AllowAnyMethod() - .AllowCredentials(); // 与 AllowAnyOrigin 不兼容 -``` - -**修复建议:** -```csharp -if (allowAnyOrigin) -{ - policy.AllowAnyOrigin() - .AllowAnyHeader() - .AllowAnyMethod(); - // 不允许 AllowCredentials -} -else -{ - policy.WithOrigins(allowedOrigins) - .AllowAnyHeader() - .AllowAnyMethod() - .AllowCredentials(); -} -``` - ---- - -### 9. 健康检查端点信息泄露 - -**文件:** `src/Program.cs` 第 115 行 - -**修复建议:** -```csharp -// 添加访问限制或使用标准健康检查 -builder.Services.AddHealthChecks(); -app.MapHealthChecks("/health", new HealthCheckOptions -{ - ResponseWriter = async (c, r) => - await c.Response.WriteAsync("healthy") -}); -``` - ---- - -### 10. JWT Authority 使用占位符 URL - -**文件:** `src/appsettings.json` 第 22 行 - -**问题代码:** -```json -"Authority": "https://your-auth-server.com" -``` - -**修复建议:** -- 强制要求配置有效的 Authority URL -- 启动时验证配置有效性 - ---- - -## 🟢 低危漏洞 - -### 11. 可预测的 ID 生成 - -**文件:** `src/Controllers/GatewayConfigController.cs` 第 484-487 行 - -**问题代码:** -```csharp -private long GenerateId() -{ - return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); -} -``` - -**修复建议:** -```csharp -// 使用 GUID 或雪花算法 -private long GenerateId() => SnowflakeIdGenerator.NextId(); -// 或 -private string GenerateId() => Guid.NewGuid().ToString("N"); -``` - ---- - -### 12. 缺少输入验证 - -**文件:** `src/Controllers/GatewayConfigController.cs` 多处 - -**修复建议:** -```csharp -public class CreateTenantDto -{ - [Required] - [RegularExpression(@"^[a-zA-Z0-9-]{1,50}$")] - public string TenantCode { get; set; } = string.Empty; - - [Required] - [StringLength(100, MinimumLength = 1)] - public string TenantName { get; set; } = string.Empty; -} -``` - ---- - -### 13. 错误消息暴露内部信息 - -**文件:** `src/Controllers/PendingServicesController.cs` 第 116 行 - -**修复建议:** -```csharp -return BadRequest(new { message = "Invalid cluster configuration" }); -``` - ---- - -## 📋 修复优先级建议 - -| 优先级 | 漏洞编号 | 修复时间建议 | -|-------|---------|------------| -| P0 (立即) | #1, #2, #3 | 24小时内 | -| P1 (紧急) | #4, #5, #6 | 1周内 | -| P2 (重要) | #7, #8, #9, #10 | 2周内 | -| P3 (一般) | #11, #12, #13 | 1个月内 | - ---- - -## 🛡️ 安全加固建议 - -### 1. 认证授权 -- 实施完整的 JWT 验证流程 -- 为所有管理 API 添加 `[Authorize]` -- 实施基于角色的访问控制 (RBAC) - -### 2. 配置安全 -- 使用 Azure Key Vault / AWS Secrets Manager 管理密钥 -- 移除所有硬编码凭据 -- 生产环境禁用调试模式 - -### 3. 租户隔离 -- 在网关层强制验证租户归属 -- 使用加密签名验证内部 Header -- 实施租户数据隔离审计 - -### 4. 日志安全 -- 敏感信息脱敏 -- 限制日志访问权限 -- 使用结构化日志便于审计 - ---- - -*报告由安全审计生成,建议人工复核后纳入迭代计划。* \ No newline at end of file diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md deleted file mode 100644 index 4db5082..0000000 --- a/.planning/codebase/STACK.md +++ /dev/null @@ -1,189 +0,0 @@ -# YARP 网关技术栈文档 - -## 1. 语言和运行时 - -### .NET 版本 -- **目标框架**: .NET 10.0 -- **项目文件**: `src/YarpGateway.csproj` -- **SDK**: `Microsoft.NET.Sdk.Web` - -```xml -net10.0 -enable -enable -``` - -## 2. 核心框架 - -### YARP (Yet Another Reverse Proxy) -- **包**: `Yarp.ReverseProxy` -- **用途**: 微服务 API 网关核心反向代理引擎 -- **主要功能**: - - 动态路由配置 - - 负载均衡策略 - - 健康检查 - - 请求转发 - -### ASP.NET Core -- **用途**: Web 应用宿主框架 -- **特性**: - - 依赖注入 (DI) - - 中间件管道 - - 配置系统 - - 日志集成 - -## 3. 主要依赖包 - -### 数据访问 -| 包名 | 用途 | -|------|------| -| `Npgsql.EntityFrameworkCore.PostgreSQL` | PostgreSQL Entity Framework Core 提供程序 | -| `Microsoft.EntityFrameworkCore.Design` | EF Core 设计时工具(迁移) | - -### 缓存与分布式锁 -| 包名 | 用途 | -|------|------| -| `StackExchange.Redis` | Redis 客户端,用于分布式锁和缓存 | - -### 认证授权 -| 包名 | 用途 | -|------|------| -| `Microsoft.AspNetCore.Authentication.JwtBearer` | JWT Bearer 认证支持 | - -### 日志 -| 包名 | 用途 | -|------|------| -| `Serilog.AspNetCore` | Serilog ASP.NET Core 集成 | -| `Serilog.Sinks.Console` | 控制台日志输出 | -| `Serilog.Sinks.File` | 文件日志输出 | - -### 服务发现(自定义包) -| 包名 | 用途 | -|------|------| -| `Fengling.ServiceDiscovery.Core` | 服务发现核心接口 | -| `Fengling.ServiceDiscovery.Kubernetes` | Kubernetes 服务发现实现 | -| `Fengling.ServiceDiscovery.Static` | 静态配置服务发现 | - -## 4. 配置文件 - -### 主配置文件 -**位置**: `src/appsettings.json` - -```json -{ - "ConnectionStrings": { - "DefaultConnection": "Host=...;Port=...;Database=...;Username=...;Password=..." - }, - "Jwt": { - "Authority": "https://your-auth-server.com", - "Audience": "fengling-gateway", - "ValidateIssuer": true, - "ValidateAudience": true - }, - "Redis": { - "ConnectionString": "host:port", - "Database": 0, - "InstanceName": "YarpGateway" - }, - "Cors": { - "AllowedOrigins": ["http://localhost:5173"], - "AllowAnyOrigin": false - }, - "Kestrel": { - "Endpoints": { - "Http": { "Url": "http://0.0.0.0:8080" } - } - }, - "Serilog": { - "MinimumLevel": "Information", - "WriteTo": [ - { "Name": "Console" }, - { "Name": "File", "Args": { "path": "logs/gateway-.log", "rollingInterval": "Day" } } - ] - } -} -``` - -### 配置类 -| 文件路径 | 类名 | 用途 | -|----------|------|------| -| `src/Config/JwtConfig.cs` | `JwtConfig` | JWT 认证配置 | -| `src/Config/RedisConfig.cs` | `RedisConfig` | Redis 连接配置 | -| `src/Config/ConfigNotifyChannel.cs` | `ConfigNotifyChannel` | PostgreSQL NOTIFY 通道常量 | - -## 5. Docker 支持 - -### Dockerfile -**位置**: `Dockerfile` - -```dockerfile -# 基础镜像 -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -EXPOSE 8080 -EXPOSE 8081 - -# 构建镜像 -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -# 多阶段构建... - -# 最终镜像 -FROM base AS final -ENTRYPOINT ["dotnet", "YarpGateway.dll"] -``` - -### Docker 配置 -- **默认目标 OS**: Linux -- **暴露端口**: 8080 (HTTP), 8081 (HTTPS) -- **工作目录**: `/app` - -## 6. 项目结构 - -``` -src/ -├── Config/ # 配置类 -│ ├── JwtConfig.cs -│ ├── RedisConfig.cs -│ ├── ConfigNotifyChannel.cs -│ ├── DatabaseRouteConfigProvider.cs -│ └── DatabaseClusterConfigProvider.cs -├── Data/ # 数据访问层 -│ ├── GatewayDbContext.cs -│ └── GatewayDbContextFactory.cs -├── DynamicProxy/ # 动态代理配置 -│ └── DynamicProxyConfigProvider.cs -├── LoadBalancing/ # 负载均衡策略 -│ └── DistributedWeightedRoundRobinPolicy.cs -├── Middleware/ # 中间件 -│ ├── JwtTransformMiddleware.cs -│ └── TenantRoutingMiddleware.cs -├── Models/ # 数据模型 -│ ├── GwTenant.cs -│ ├── GwTenantRoute.cs -│ ├── GwServiceInstance.cs -│ └── GwPendingServiceDiscovery.cs -├── Services/ # 业务服务 -│ ├── RouteCache.cs -│ ├── RedisConnectionManager.cs -│ ├── KubernetesPendingSyncService.cs -│ └── PgSqlConfigChangeListener.cs -├── Program.cs # 应用入口 -├── appsettings.json # 配置文件 -└── YarpGateway.csproj # 项目文件 -``` - -## 7. 中间件管道 - -请求处理管道顺序(`Program.cs`): - -1. **CORS** - 跨域请求处理 -2. **JwtTransformMiddleware** - JWT 解析与转换 -3. **TenantRoutingMiddleware** - 租户路由解析 -4. **Controllers** - API 控制器 -5. **ReverseProxy** - YARP 反向代理 - -## 8. 托管与部署 - -### Kestrel 配置 -- 监听地址: `http://0.0.0.0:8080` -- 支持 Docker 容器化部署 -- 支持 Kubernetes 集群部署 \ No newline at end of file diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md deleted file mode 100644 index 8b9cdba..0000000 --- a/.planning/codebase/STRUCTURE.md +++ /dev/null @@ -1,465 +0,0 @@ -# YARP Gateway 目录结构文档 - -## 1. 目录布局 - -``` -fengling-gateway/ -├── .planning/ # 规划文档目录 -│ └── codebase/ # 代码库分析文档 -│ ├── ARCHITECTURE.md # 架构文档 -│ └── STRUCTURE.md # 本文档 -│ -├── src/ # 源代码目录 -│ ├── Config/ # 配置类和提供者 -│ ├── Controllers/ # API 控制器 -│ ├── Data/ # 数据访问层 -│ ├── DynamicProxy/ # YARP 动态代理 -│ ├── LoadBalancing/ # 负载均衡策略 -│ ├── Migrations/ # 数据库迁移 -│ ├── Metrics/ # 监控指标 -│ ├── Middleware/ # 中间件 -│ ├── Models/ # 数据模型 -│ ├── Properties/ # 项目属性 -│ ├── Services/ # 业务服务 -│ ├── Program.cs # 程序入口 -│ ├── YarpGateway.csproj # 项目文件 -│ ├── appsettings.json # 配置文件 -│ └── appsettings.Development.json # 开发环境配置 -│ -└── (根目录其他文件) -``` - ---- - -## 2. 详细目录说明 - -### 2.1 Config/ - 配置层 - -**路径**: `src/Config/` - -**用途**: 存放配置模型和配置提供者 - -| 文件 | 行数 | 用途 | -|------|------|------| -| `JwtConfig.cs` | 10 | JWT 认证配置模型,包含 Authority、Audience 等属性 | -| `RedisConfig.cs` | 9 | Redis 连接配置模型,包含连接字符串、数据库索引等 | -| `ConfigNotifyChannel.cs` | 7 | PostgreSQL NOTIFY 通道名称常量定义 | -| `DatabaseRouteConfigProvider.cs` | 84 | 从数据库加载路由配置,转换为 YARP RouteConfig | -| `DatabaseClusterConfigProvider.cs` | 100 | 从数据库加载集群配置,管理服务实例列表 | - -**设计特点**: -- 配置类使用 POCO 模型,通过 Options 模式注入 -- Provider 类使用单例模式,支持热重载 - ---- - -### 2.2 Controllers/ - 控制器层 - -**路径**: `src/Controllers/` - -**用途**: RESTful API 端点 - -| 文件 | 行数 | 路由前缀 | 用途 | -|------|------|----------|------| -| `GatewayConfigController.cs` | 489 | `/api/gateway` | 网关配置管理 API | -| `PendingServicesController.cs` | 210 | `/api/gateway/pending-services` | 待处理服务管理 API | - -**GatewayConfigController 端点**: - -| 方法 | 路由 | 功能 | -|------|------|------| -| GET | `/tenants` | 获取租户列表(分页) | -| GET | `/tenants/{id}` | 获取单个租户 | -| POST | `/tenants` | 创建租户 | -| PUT | `/tenants/{id}` | 更新租户 | -| DELETE | `/tenants/{id}` | 删除租户 | -| GET | `/routes` | 获取路由列表(分页) | -| GET | `/routes/global` | 获取全局路由 | -| GET | `/routes/tenant/{tenantCode}` | 获取租户路由 | -| POST | `/routes` | 创建路由 | -| PUT | `/routes/{id}` | 更新路由 | -| DELETE | `/routes/{id}` | 删除路由 | -| GET | `/clusters` | 获取集群列表 | -| GET | `/clusters/{clusterId}` | 获取集群详情 | -| POST | `/clusters` | 创建集群 | -| DELETE | `/clusters/{clusterId}` | 删除集群 | -| GET | `/clusters/{clusterId}/instances` | 获取实例列表 | -| POST | `/clusters/{clusterId}/instances` | 添加实例 | -| DELETE | `/instances/{id}` | 删除实例 | -| POST | `/config/reload` | 重载配置 | -| GET | `/config/status` | 获取配置状态 | -| GET | `/config/versions` | 获取版本信息 | -| GET | `/stats/overview` | 获取统计概览 | - -**PendingServicesController 端点**: - -| 方法 | 路由 | 功能 | -|------|------|------| -| GET | `/` | 获取待处理服务列表 | -| GET | `/{id}` | 获取待处理服务详情 | -| POST | `/{id}/assign` | 分配服务到集群 | -| POST | `/{id}/reject` | 拒绝服务 | -| GET | `/clusters` | 获取可用集群列表 | - ---- - -### 2.3 Data/ - 数据访问层 - -**路径**: `src/Data/` - -**用途**: Entity Framework Core 数据库上下文 - -| 文件 | 行数 | 用途 | -|------|------|------| -| `GatewayDbContext.cs` | 142 | EF Core 数据库上下文,包含实体配置和变更通知 | -| `GatewayDbContextFactory.cs` | 23 | 设计时 DbContext 工厂,用于迁移命令 | - -**DbContext 特性**: -- 自动检测配置变更 -- 集成 PostgreSQL NOTIFY 机制 -- 支持软删除(IsDeleted 标记) -- 版本号追踪(Version 字段) - ---- - -### 2.4 DynamicProxy/ - 动态代理层 - -**路径**: `src/DynamicProxy/` - -**用途**: YARP 动态配置提供 - -| 文件 | 行数 | 用途 | -|------|------|------| -| `DynamicProxyConfigProvider.cs` | 79 | 实现 IProxyConfigProvider,整合路由和集群配置 | - -**核心职责**: -- 实现 YARP 配置提供接口 -- 协调 Route 和 Cluster 配置 -- 提供配置变更通知(通过 CancellationToken) - ---- - -### 2.5 LoadBalancing/ - 负载均衡层 - -**路径**: `src/LoadBalancing/` - -**用途**: 自定义负载均衡策略 - -| 文件 | 行数 | 用途 | -|------|------|------| -| `DistributedWeightedRoundRobinPolicy.cs` | 244 | 基于 Redis 的分布式加权轮询策略 | - -**策略特点**: -- 策略名称: `DistributedWeightedRoundRobin` -- 支持实例权重配置 -- Redis 分布式状态存储 -- 降级策略(锁获取失败时) - ---- - -### 2.6 Migrations/ - 数据库迁移 - -**路径**: `src/Migrations/` - -**用途**: Entity Framework Core 迁移文件 - -| 文件 | 用途 | -|------|------| -| `20260201120312_InitialCreate.cs` | 初始数据库创建 | -| `20260201133826_AddIsGlobalToTenantRoute.cs` | 添加 IsGlobal 字段 | -| `20260222134342_AddPendingServiceDiscovery.cs` | 添加待处理服务发现表 | -| `*ModelSnapshot.cs` | 当前模型快照 | -| `*.Designer.cs` | 设计器生成文件 | - ---- - -### 2.7 Metrics/ - 监控指标 - -**路径**: `src/Metrics/` - -**用途**: OpenTelemetry 指标定义 - -| 文件 | 行数 | 用途 | -|------|------|------| -| `GatewayMetrics.cs` | 31 | 定义网关监控指标 | - -**指标列表**: -- `gateway_requests_total` - 请求总数计数器 -- `gateway_request_duration_seconds` - 请求延迟直方图 - ---- - -### 2.8 Middleware/ - 中间件层 - -**路径**: `src/Middleware/` - -**用途**: ASP.NET Core 中间件 - -| 文件 | 行数 | 用途 | -|------|------|------| -| `JwtTransformMiddleware.cs` | 84 | JWT Token 解析,提取租户信息注入请求头 | -| `TenantRoutingMiddleware.cs` | 64 | 租户路由解析,根据路径查找目标集群 | - -**中间件执行顺序**: -``` -CORS -> JwtTransformMiddleware -> TenantRoutingMiddleware -> YARP -``` - ---- - -### 2.9 Models/ - 数据模型层 - -**路径**: `src/Models/` - -**用途**: 实体类定义 - -| 文件 | 行数 | 用途 | -|------|------|------| -| `GwTenant.cs` | 16 | 租户实体 | -| `GwTenantRoute.cs` | 20 | 路由配置实体 | -| `GwServiceInstance.cs` | 19 | 服务实例实体 | -| `GwPendingServiceDiscovery.cs` | 28 | 待处理服务发现实体 + 状态枚举 | - -**实体通用字段**: -- `Id` - 主键(雪花 ID 格式) -- `Status` - 状态(1=启用) -- `CreatedBy/UpdatedBy` - 操作人 -- `CreatedTime/UpdatedTime` - 时间戳 -- `IsDeleted` - 软删除标记 -- `Version` - 版本号(乐观锁) - ---- - -### 2.10 Services/ - 服务层 - -**路径**: `src/Services/` - -**用途**: 业务逻辑和后台服务 - -| 文件 | 行数 | 类型 | 用途 | -|------|------|------|------| -| `RouteCache.cs` | 139 | Singleton | 路由缓存,支持租户路由和全局路由 | -| `RedisConnectionManager.cs` | 139 | Singleton | Redis 连接管理,分布式锁实现 | -| `PgSqlConfigChangeListener.cs` | 223 | HostedService | PostgreSQL 配置变更监听 | -| `KubernetesPendingSyncService.cs` | 162 | HostedService | Kubernetes 服务发现同步 | - -**服务生命周期**: -- Singleton: RouteCache, RedisConnectionManager(状态服务) -- HostedService: PgSqlConfigChangeListener, KubernetesPendingSyncService(后台任务) - ---- - -## 3. 关键文件位置 - -### 3.1 入口文件 - -| 文件 | 路径 | 用途 | -|------|------|------| -| `Program.cs` | `src/Program.cs` | 应用程序入口,服务注册和中间件配置 | - -### 3.2 配置文件 - -| 文件 | 路径 | 用途 | -|------|------|------| -| `appsettings.json` | `src/appsettings.json` | 生产环境配置 | -| `appsettings.Development.json` | `src/appsettings.Development.json` | 开发环境配置 | -| `YarpGateway.csproj` | `src/YarpGateway.csproj` | 项目文件,包引用 | - -### 3.3 数据库相关 - -| 文件 | 路径 | 用途 | -|------|------|------| -| `GatewayDbContext.cs` | `src/Data/GatewayDbContext.cs` | 数据库上下文 | -| `GatewayDbContextFactory.cs` | `src/Data/GatewayDbContextFactory.cs` | 迁移工具工厂 | - ---- - -## 4. 命名约定 - -### 4.1 文件命名 - -| 类型 | 命名规则 | 示例 | -|------|----------|------| -| 实体类 | `Gw` 前缀 + PascalCase | `GwTenant.cs`, `GwTenantRoute.cs` | -| 配置类 | `*Config` 后缀 | `JwtConfig.cs`, `RedisConfig.cs` | -| 提供者 | `*Provider` 后缀 | `DatabaseRouteConfigProvider.cs` | -| 中间件 | `*Middleware` 后缀 | `JwtTransformMiddleware.cs` | -| 控制器 | `*Controller` 后缀 | `GatewayConfigController.cs` | -| 服务 | 功能描述 + 类型 | `RouteCache.cs`, `PgSqlConfigChangeListener.cs` | -| 策略 | `*Policy` 后缀 | `DistributedWeightedRoundRobinPolicy.cs` | - -### 4.2 命名空间 - -``` -YarpGateway # 根命名空间 -├── Config # 配置相关 -├── Controllers # API 控制器 -├── Data # 数据访问 -├── DynamicProxy # 动态代理 -├── LoadBalancing # 负载均衡 -├── Metrics # 监控指标 -├── Middleware # 中间件 -├── Models # 数据模型 -└── Services # 业务服务 -``` - -### 4.3 接口命名 - -| 类型 | 命名规则 | 示例 | -|------|----------|------| -| 服务接口 | `I` 前缀 | `IRouteCache`, `IRedisConnectionManager` | -| DTO 类 | `*Dto` 后缀 | `CreateTenantDto`, `CreateRouteDto` | -| 请求类 | `*Request` 后缀 | `AssignServiceRequest` | - ---- - -## 5. 模块组织 - -### 5.1 分层架构 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Presentation Layer │ -│ ┌─────────────────┐ ┌─────────────────────────────────┐ │ -│ │ Middleware │ │ Controllers │ │ -│ │ - JWT 解析 │ │ - GatewayConfigController │ │ -│ │ - 租户路由 │ │ - PendingServicesController │ │ -│ └─────────────────┘ └─────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Business Logic Layer │ -│ ┌─────────────────┐ ┌─────────────────────────────────┐ │ -│ │ Services │ │ DynamicProxy │ │ -│ │ - RouteCache │ │ - DynamicProxyConfigProvider │ │ -│ │ - RedisManager │ │ │ │ -│ │ - ConfigListen │ └─────────────────────────────────┘ │ -│ └─────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Data Access Layer │ -│ ┌─────────────────┐ ┌─────────────────────────────────┐ │ -│ │ Models │ │ Data │ │ -│ │ - GwTenant │ │ - GatewayDbContext │ │ -│ │ - GwRoute │ │ - GatewayDbContextFactory │ │ -│ │ - GwInstance │ │ │ │ -│ └─────────────────┘ └─────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Infrastructure Layer │ -│ ┌─────────────────┐ ┌─────────────────────────────────┐ │ -│ │ Config │ │ LoadBalancing │ │ -│ │ - JwtConfig │ │ - WeightedRoundRobinPolicy │ │ -│ │ - RedisConfig │ │ │ │ -│ │ - Providers │ └─────────────────────────────────┘ │ -│ └─────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 5.2 模块依赖关系 - -``` -Program.cs - │ - ├── Config/ - │ ├── JwtConfig ◄── appsettings.json - │ ├── RedisConfig ◄── appsettings.json - │ ├── DatabaseRouteConfigProvider ◄── Data/GatewayDbContext - │ └── DatabaseClusterConfigProvider ◄── Data/GatewayDbContext - │ - ├── DynamicProxy/ - │ └── DynamicProxyConfigProvider ◄── Config/* - │ - ├── Services/ - │ ├── RouteCache ◄── Data/GatewayDbContext, Models/* - │ ├── RedisConnectionManager ◄── Config/RedisConfig - │ ├── PgSqlConfigChangeListener ◄── DynamicProxy, Services/RouteCache - │ └── KubernetesPendingSyncService ◄── Data/GatewayDbContext - │ - ├── Middleware/ - │ ├── JwtTransformMiddleware ◄── Config/JwtConfig - │ └── TenantRoutingMiddleware ◄── Services/RouteCache - │ - └── Controllers/ - ├── GatewayConfigController ◄── Config/*, Services/RouteCache - └── PendingServicesController ◄── Data/GatewayDbContext -``` - ---- - -## 6. 项目依赖 - -### 6.1 NuGet 包引用 - -```xml - - - - - - - - - - - - - - - - - - - - -``` - -### 6.2 目标框架 - -```xml -net10.0 -``` - ---- - -## 7. 文件统计 - -| 目录/文件 | 文件数 | 总行数 | 主要用途 | -|-----------|--------|--------|----------| -| `Config/` | 5 | ~210 | 配置模型和提供者 | -| `Controllers/` | 2 | ~700 | REST API 端点 | -| `Data/` | 2 | ~165 | 数据库上下文 | -| `DynamicProxy/` | 1 | ~79 | YARP 配置集成 | -| `LoadBalancing/` | 1 | ~244 | 负载均衡策略 | -| `Migrations/` | 6 | ~500+ | 数据库迁移 | -| `Metrics/` | 1 | ~31 | 监控指标 | -| `Middleware/` | 2 | ~148 | 请求处理中间件 | -| `Models/` | 4 | ~83 | 数据实体 | -| `Services/` | 4 | ~665 | 业务服务 | -| `Program.cs` | 1 | 135 | 应用入口 | -| **总计** | **29** | **~2900+** | - | - ---- - -## 8. 扩展建议 - -### 8.1 建议新增目录 - -| 目录 | 用途 | -|------|------| -| `Extensions/` | 扩展方法 | -| `Constants/` | 常量定义 | -| `Exceptions/` | 自定义异常 | -| `Validators/` | 输入验证器 | -| `Dtos/` | 数据传输对象(从 Controllers 提取) | - -### 8.2 代码组织建议 - -1. 将 Controller 中的 DTO 类提取到独立的 `Dtos/` 目录 -2. 添加 `Extensions/` 存放 IServiceCollection 扩展方法 -3. 考虑将配置验证逻辑提取到 `Validators/` \ No newline at end of file diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md deleted file mode 100644 index 6056908..0000000 --- a/.planning/codebase/TESTING.md +++ /dev/null @@ -1,833 +0,0 @@ -# YARP Gateway 测试文档 - -## 概述 - -本文档记录了 YARP Gateway 项目的测试策略、测试模式和最佳实践。 - ---- - -## 1. 测试框架 - -### 1.1 当前测试状态 - -**项目当前没有专门的测试目录或测试项目。** - -检查项目结构: -``` -fengling-gateway/ -├── src/ # 源代码 -│ └── YarpGateway.csproj # 主项目 -├── .planning/ -└── (无 tests/ 或 test/ 目录) -``` - -检查 `.csproj` 文件确认无测试框架依赖: -```xml - - - - - - - - -``` - -**结论**:项目目前处于开发阶段,尚未建立测试基础设施。 - -### 1.2 推荐测试框架 - -基于项目技术栈,推荐以下测试框架: - -| 框架 | 用途 | NuGet 包 | -|------|------|----------| -| xUnit | 单元测试框架 | `xunit` | -| Moq | Mock 框架 | `Moq` | -| FluentAssertions | 断言库 | `FluentAssertions` | -| Microsoft.NET.Test.Sdk | 测试 SDK | `Microsoft.NET.Test.Sdk` | -| Testcontainers | 集成测试容器 | `Testcontainers.PostgreSql`, `Testcontainers.Redis` | - ---- - -## 2. 推荐测试结构 - -### 2.1 测试项目组织 - -建议创建独立的测试项目: - -``` -tests/ -├── YarpGateway.UnitTests/ # 单元测试 -│ ├── Services/ -│ │ ├── RouteCacheTests.cs -│ │ └── RedisConnectionManagerTests.cs -│ ├── Middleware/ -│ │ ├── JwtTransformMiddlewareTests.cs -│ │ └── TenantRoutingMiddlewareTests.cs -│ └── Controllers/ -│ └── GatewayConfigControllerTests.cs -│ -├── YarpGateway.IntegrationTests/ # 集成测试 -│ ├── GatewayEndpointsTests.cs -│ └── DatabaseTests.cs -│ -└── YarpGateway.LoadTests/ # 负载测试(可选) - └── RoutePerformanceTests.cs -``` - -### 2.2 测试命名约定 - -```csharp -// 命名格式:[被测类]Tests -public class RouteCacheTests { } - -// 方法命名格式:[方法名]_[场景]_[期望结果] -[Fact] -public async Task InitializeAsync_WithValidData_LoadsRoutesFromDatabase() { } - -[Fact] -public async Task GetRoute_WithNonexistentTenant_ReturnsNull() { } - -[Fact] -public async Task ReloadAsync_WhenCalled_RefreshesCache() { } -``` - ---- - -## 3. 单元测试模式 - -### 3.1 服务层测试示例 - -```csharp -// RouteCacheTests.cs -using Xunit; -using Moq; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -public class RouteCacheTests -{ - private readonly Mock> _mockDbContextFactory; - private readonly Mock> _mockLogger; - private readonly RouteCache _sut; // System Under Test - - public RouteCacheTests() - { - _mockDbContextFactory = new Mock>(); - _mockLogger = new Mock>(); - _sut = new RouteCache(_mockDbContextFactory.Object, _mockLogger.Object); - } - - [Fact] - public async Task InitializeAsync_ShouldLoadRoutesFromDatabase() - { - // Arrange - var routes = new List - { - new() { Id = 1, ServiceName = "user-service", ClusterId = "user-cluster", IsGlobal = true } - }; - - var mockDbSet = CreateMockDbSet(routes); - var mockContext = new Mock(); - mockContext.Setup(c => c.TenantRoutes).Returns(mockDbSet.Object); - - _mockDbContextFactory - .Setup(f => f.CreateDbContext()) - .Returns(mockContext.Object); - - // Act - await _sut.InitializeAsync(); - - // Assert - var result = _sut.GetRoute("tenant1", "user-service"); - result.Should().NotBeNull(); - result!.ClusterId.Should().Be("user-cluster"); - } - - [Fact] - public async Task GetRoute_WhenTenantRouteExists_ReturnsTenantRoute() - { - // Arrange - 设置租户专用路由 - // ... - - // Act - var result = _sut.GetRoute("tenant1", "service1"); - - // Assert - result.Should().NotBeNull(); - result!.IsGlobal.Should().BeFalse(); - } - - [Fact] - public async Task GetRoute_WhenNoTenantRouteButGlobalExists_ReturnsGlobalRoute() - { - // Arrange - // ... - - // Act - var result = _sut.GetRoute("tenant-without-route", "global-service"); - - // Assert - result.Should().NotBeNull(); - result!.IsGlobal.Should().BeTrue(); - } - - // 辅助方法:创建模拟 DbSet - private Mock> CreateMockDbSet(List data) where T : class - { - var queryable = data.AsQueryable(); - var mockSet = new Mock>(); - mockSet.As>().Setup(m => m.Provider).Returns(queryable.Provider); - mockSet.As>().Setup(m => m.Expression).Returns(queryable.Expression); - mockSet.As>().Setup(m => m.ElementType).Returns(queryable.ElementType); - mockSet.As>().Setup(m => m.GetEnumerator()).Returns(queryable.GetEnumerator()); - return mockSet; - } -} -``` - -### 3.2 中间件测试示例 - -```csharp -// TenantRoutingMiddlewareTests.cs -using Xunit; -using Moq; -using FluentAssertions; -using Microsoft.AspNetCore.Http; - -public class TenantRoutingMiddlewareTests -{ - private readonly Mock _mockNext; - private readonly Mock _mockRouteCache; - private readonly Mock> _mockLogger; - private readonly TenantRoutingMiddleware _sut; - - public TenantRoutingMiddlewareTests() - { - _mockNext = new Mock(); - _mockRouteCache = new Mock(); - _mockLogger = new Mock>(); - _sut = new TenantRoutingMiddleware(_mockNext.Object, _mockRouteCache.Object, _mockLogger.Object); - } - - [Fact] - public async Task InvokeAsync_WithoutTenantHeader_CallsNextWithoutProcessing() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.Path = "/api/user-service/users"; - - // Act - await _sut.InvokeAsync(context); - - // Assert - _mockNext.Verify(n => n(context), Times.Once); - _mockRouteCache.Verify(r => r.GetRoute(It.IsAny(), It.IsAny()), Times.Never); - } - - [Fact] - public async Task InvokeAsync_WithValidTenantAndRoute_SetsDynamicClusterId() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.Path = "/api/order-service/orders"; - context.Request.Headers["X-Tenant-Id"] = "tenant-123"; - - var routeInfo = new RouteInfo - { - ClusterId = "order-cluster", - IsGlobal = false - }; - - _mockRouteCache - .Setup(r => r.GetRoute("tenant-123", "order-service")) - .Returns(routeInfo); - - // Act - await _sut.InvokeAsync(context); - - // Assert - context.Items["DynamicClusterId"].Should().Be("order-cluster"); - _mockNext.Verify(n => n(context), Times.Once); - } - - [Fact] - public async Task InvokeAsync_WithNoMatchingRoute_CallsNextWithoutClusterId() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.Path = "/api/unknown-service/data"; - context.Request.Headers["X-Tenant-Id"] = "tenant-123"; - - _mockRouteCache - .Setup(r => r.GetRoute("tenant-123", "unknown-service")) - .Returns((RouteInfo?)null); - - // Act - await _sut.InvokeAsync(context); - - // Assert - context.Items.ContainsKey("DynamicClusterId").Should().BeFalse(); - _mockNext.Verify(n => n(context), Times.Once); - } -} -``` - -### 3.3 控制器测试示例 - -```csharp -// GatewayConfigControllerTests.cs -using Xunit; -using Moq; -using FluentAssertions; -using Microsoft.AspNetCore.Mvc; - -public class GatewayConfigControllerTests -{ - private readonly Mock> _mockDbFactory; - private readonly Mock _mockRouteProvider; - private readonly Mock _mockClusterProvider; - private readonly Mock _mockRouteCache; - private readonly GatewayConfigController _sut; - - public GatewayConfigControllerTests() - { - _mockDbFactory = new Mock>(); - _mockRouteProvider = new Mock(); - _mockClusterProvider = new Mock(); - _mockRouteCache = new Mock(); - - _sut = new GatewayConfigController( - _mockDbFactory.Object, - _mockRouteProvider.Object, - _mockClusterProvider.Object, - _mockRouteCache.Object - ); - } - - [Fact] - public async Task GetTenants_ShouldReturnPaginatedList() - { - // Arrange - var tenants = new List - { - new() { Id = 1, TenantCode = "tenant1", TenantName = "Tenant 1" }, - new() { Id = 2, TenantCode = "tenant2", TenantName = "Tenant 2" } - }; - - // 设置模拟 DbContext... - - // Act - var result = await _sut.GetTenants(page: 1, pageSize: 10); - - // Assert - var okResult = result.Should().BeOfType().Subject; - var response = okResult.Value.Should().BeAnonymousType(); - response.Property("total").Should().Be(2); - } - - [Fact] - public async Task CreateTenant_WithValidData_ReturnsCreatedTenant() - { - // Arrange - var dto = new GatewayConfigController.CreateTenantDto - { - TenantCode = "new-tenant", - TenantName = "New Tenant" - }; - - // Act - var result = await _sut.CreateTenant(dto); - - // Assert - var okResult = result.Should().BeOfType().Subject; - okResult.Value.Should().BeAssignableTo(); - } - - [Fact] - public async Task DeleteTenant_WithNonexistentId_ReturnsNotFound() - { - // Arrange - // 设置模拟返回 null - - // Act - var result = await _sut.DeleteTenant(999); - - // Assert - result.Should().BeOfType(); - } -} -``` - ---- - -## 4. Mock 模式 - -### 4.1 接口 Mock - -```csharp -// 使用 Moq 模拟接口 -public class RouteCacheTests -{ - private readonly Mock _mockRouteCache; - - public RouteCacheTests() - { - _mockRouteCache = new Mock(); - } - - [Fact] - public async Task TestMethod() - { - // 设置返回值 - _mockRouteCache - .Setup(r => r.GetRoute("tenant1", "service1")) - .Returns(new RouteInfo { ClusterId = "cluster1" }); - - // 设置异步方法 - _mockRouteCache - .Setup(r => r.InitializeAsync()) - .Returns(Task.CompletedTask); - - // 验证调用 - _mockRouteCache.Verify(r => r.GetRoute(It.IsAny(), It.IsAny()), Times.Once); - } -} -``` - -### 4.2 DbContext Mock - -```csharp -// 使用 In-Memory 数据库进行测试 -public class TestDatabaseFixture -{ - public GatewayDbContext CreateContext() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) - .Options; - - var context = new GatewayDbContext(options); - - // 种子数据 - context.Tenants.Add(new GwTenant { Id = 1, TenantCode = "test-tenant" }); - context.TenantRoutes.Add(new GwTenantRoute - { - Id = 1, - ServiceName = "test-service", - ClusterId = "test-cluster" - }); - context.SaveChanges(); - - return context; - } -} - -public class GatewayDbContextTests : IClassFixture -{ - private readonly TestDatabaseFixture _fixture; - - public GatewayDbContextTests(TestDatabaseFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task SaveChangesAsync_ShouldNotifyConfigChange() - { - // Arrange - await using var context = _fixture.CreateContext(); - - // Act - var route = new GwTenantRoute { ServiceName = "new-service", ClusterId = "new-cluster" }; - context.TenantRoutes.Add(route); - await context.SaveChangesAsync(); - - // Assert - // 验证通知行为(如果需要) - } -} -``` - -### 4.3 Redis Mock - -```csharp -// 使用 Moq 模拟 Redis -public class RedisConnectionManagerTests -{ - private readonly Mock _mockRedis; - private readonly Mock _mockDatabase; - - public RedisConnectionManagerTests() - { - _mockRedis = new Mock(); - _mockDatabase = new Mock(); - _mockRedis.Setup(r => r.GetDatabase(It.IsAny(), It.IsAny())) - .Returns(_mockDatabase.Object); - } - - [Fact] - public async Task AcquireLockAsync_WhenLockAvailable_ReturnsDisposable() - { - // Arrange - _mockDatabase - .Setup(d => d.StringSetAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(true); - - // Act & Assert - // 测试逻辑... - } -} -``` - ---- - -## 5. 集成测试模式 - -### 5.1 WebApplicationFactory 模式 - -```csharp -// 使用 WebApplicationFactory 进行 API 集成测试 -using Microsoft.AspNetCore.Mvc.Testing; - -public class GatewayIntegrationTests : IClassFixture> -{ - private readonly WebApplicationFactory _factory; - private readonly HttpClient _client; - - public GatewayIntegrationTests(WebApplicationFactory factory) - { - _factory = factory.WithWebHostBuilder(builder => - { - builder.ConfigureServices(services => - { - // 替换真实服务为测试替身 - services.RemoveAll>(); - services.AddDbContextFactory(options => - options.UseInMemoryDatabase("TestDb")); - }); - }); - _client = _factory.CreateClient(); - } - - [Fact] - public async Task GetHealth_ReturnsHealthy() - { - // Act - var response = await _client.GetAsync("/health"); - - // Assert - response.Should().BeSuccessful(); - var content = await response.Content.ReadAsStringAsync(); - content.Should().Contain("healthy"); - } - - [Fact] - public async Task GetTenants_ReturnsPaginatedList() - { - // Act - var response = await _client.GetAsync("/api/gateway/tenants?page=1&pageSize=10"); - - // Assert - response.Should().BeSuccessful(); - // 进一步验证响应内容... - } -} -``` - -### 5.2 Testcontainers 模式 - -```csharp -// 使用 Testcontainers 进行真实数据库集成测试 -using Testcontainers.PostgreSql; -using Testcontainers.Redis; - -public class DatabaseIntegrationTests : IAsyncLifetime -{ - private readonly PostgreSqlContainer _postgresContainer; - private readonly RedisContainer _redisContainer; - - public DatabaseIntegrationTests() - { - _postgresContainer = new PostgreSqlBuilder() - .WithImage("postgres:15-alpine") - .WithDatabase("test_gateway") - .WithUsername("test") - .WithPassword("test") - .Build(); - - _redisContainer = new RedisBuilder() - .WithImage("redis:7-alpine") - .Build(); - } - - public async Task InitializeAsync() - { - await _postgresContainer.StartAsync(); - await _redisContainer.StartAsync(); - } - - public async Task DisposeAsync() - { - await _postgresContainer.DisposeAsync(); - await _redisContainer.DisposeAsync(); - } - - [Fact] - public async Task FullWorkflow_CreateTenantAndRoute_RouteShouldWork() - { - // Arrange - var connectionString = _postgresContainer.GetConnectionString(); - - // 使用真实连接进行端到端测试... - } -} -``` - ---- - -## 6. 测试覆盖率 - -### 6.1 当前状态 - -项目当前无测试覆盖率数据。 - -### 6.2 推荐覆盖率目标 - -| 层级 | 目标覆盖率 | 说明 | -|------|-----------|------| -| Services | 80%+ | 核心业务逻辑,必须高覆盖 | -| Middleware | 75%+ | 关键请求处理逻辑 | -| Controllers | 70%+ | API 端点行为验证 | -| Config | 60%+ | 配置加载和验证 | -| Models | 30%+ | 简单 POCO 类,低优先级 | - -### 6.3 配置覆盖率收集 - -```xml - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - -``` - -```bash -# 运行测试并收集覆盖率 -dotnet test --collect:"XPlat Code Coverage" - -# 生成覆盖率报告 -dotnet tool install -g dotnet-reportgenerator-globaltool -reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"coverage-report" -``` - ---- - -## 7. 如何运行测试 - -### 7.1 运行所有测试 - -```bash -# 运行所有测试 -dotnet test - -# 运行特定项目 -dotnet test tests/YarpGateway.UnitTests - -# 运行特定测试类 -dotnet test --filter "FullyQualifiedName~RouteCacheTests" - -# 运行特定测试方法 -dotnet test --filter "FullyQualifiedName~RouteCacheTests.InitializeAsync_ShouldLoadRoutesFromDatabase" -``` - -### 7.2 运行测试类别 - -```csharp -// 定义测试类别 -[Trait("Category", "Unit")] -public class RouteCacheTests { } - -[Trait("Category", "Integration")] -public class GatewayIntegrationTests { } -``` - -```bash -# 只运行单元测试 -dotnet test --filter "Category=Unit" - -# 排除集成测试 -dotnet test --filter "Category!=Integration" -``` - -### 7.3 CI/CD 配置示例 - -```yaml -# .github/workflows/test.yml -name: Tests - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - - services: - postgres: - image: postgres:15 - env: - POSTGRES_DB: test_gateway - POSTGRES_USER: test - POSTGRES_PASSWORD: test - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - redis: - image: redis:7 - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Restore dependencies - run: dotnet restore - - - name: Build - run: dotnet build --configuration Release --no-restore - - - name: Test - run: dotnet test --configuration Release --no-build --verbosity normal --collect:"XPlat Code Coverage" - env: - ConnectionStrings__DefaultConnection: "Host=localhost;Database=test_gateway;Username=test;Password=test" - Redis__ConnectionString: "localhost:6379" - - - name: Upload coverage - uses: codecov/codecov-action@v3 - with: - files: ./tests/**/coverage.cobertura.xml -``` - ---- - -## 8. 测试最佳实践 - -### 8.1 AAA 模式 - -```csharp -[Fact] -public async Task Method_Scenario_ExpectedResult() -{ - // Arrange - 准备测试数据和环境 - var input = "test-data"; - - // Act - 执行被测试的方法 - var result = await _sut.MethodAsync(input); - - // Assert - 验证结果 - result.Should().Be(expected); -} -``` - -### 8.2 单一职责 - -```csharp -// ✅ 好:每个测试只验证一个行为 -[Fact] -public async Task CreateTenant_WithValidData_ReturnsCreatedTenant() { } - -[Fact] -public async Task CreateTenant_WithDuplicateCode_ReturnsBadRequest() { } - -// ❌ 差:一个测试验证多个行为 -[Fact] -public async Task CreateTenant_TestsAllScenarios() { } -``` - -### 8.3 测试隔离 - -```csharp -public class RouteCacheTests -{ - // 每个测试使用独立实例 - private readonly RouteCache _sut; - - public RouteCacheTests() - { - // 在构造函数中初始化,确保每个测试独立 - _sut = new RouteCache(...); - } -} -``` - -### 8.4 避免实现细节测试 - -```csharp -// ✅ 好:测试行为而非实现 -[Fact] -public async Task GetRoute_ReturnsCorrectRoute() { } - -// ❌ 差:测试内部实现细节 -[Fact] -public void InternalDictionary_ContainsCorrectKey() { } -``` - ---- - -## 9. 总结 - -### 当前状态 -- ❌ 无测试项目 -- ❌ 无测试框架依赖 -- ❌ 无测试覆盖率 -- ❌ 无 CI/CD 测试配置 - -### 建议行动计划 - -1. **创建测试项目** - ```bash - dotnet new xunit -n YarpGateway.UnitTests -o tests/YarpGateway.UnitTests - dotnet new xunit -n YarpGateway.IntegrationTests -o tests/YarpGateway.IntegrationTests - ``` - -2. **添加测试依赖** - ```bash - dotnet add package Moq - dotnet add package FluentAssertions - dotnet add package coverlet.collector - ``` - -3. **优先测试核心服务** - - `RouteCache` - 路由缓存核心逻辑 - - `RedisConnectionManager` - Redis 连接和分布式锁 - - `TenantRoutingMiddleware` - 租户路由中间件 - -4. **建立 CI/CD 测试流程** - - 每次提交运行单元测试 - - 每次合并运行集成测试 - - 生成覆盖率报告 - -通过建立完善的测试体系,可以显著提高代码质量和项目可维护性。 \ No newline at end of file diff --git a/.planning/codebase/TEST_PLAN.md b/.planning/codebase/TEST_PLAN.md deleted file mode 100644 index aa6c5f7..0000000 --- a/.planning/codebase/TEST_PLAN.md +++ /dev/null @@ -1,180 +0,0 @@ -# 🧪 YARP 网关测试覆盖计划 - -> 分析日期:2026-02-28 -> 当前状态:**无任何测试代码** - ---- - -## 测试项目结构 - -``` -tests/ -└── YarpGateway.Tests/ - ├── YarpGateway.Tests.csproj - ├── Unit/ - │ ├── Middleware/ - │ │ ├── JwtTransformMiddlewareTests.cs - │ │ └── TenantRoutingMiddlewareTests.cs - │ ├── Services/ - │ │ ├── RouteCacheTests.cs - │ │ ├── RedisConnectionManagerTests.cs - │ │ └── PgSqlConfigChangeListenerTests.cs - │ ├── LoadBalancing/ - │ │ └── DistributedWeightedRoundRobinPolicyTests.cs - │ └── Config/ - │ ├── DatabaseRouteConfigProviderTests.cs - │ └── DatabaseClusterConfigProviderTests.cs - ├── Integration/ - │ ├── Controllers/ - │ │ ├── GatewayConfigControllerTests.cs - │ │ └── PendingServicesControllerTests.cs - │ └── Middleware/ - │ └── MiddlewarePipelineTests.cs - └── TestHelpers/ - ├── MockDbContext.cs - ├── MockRedis.cs - └── TestFixtures.cs -``` - ---- - -## P0 - 必须覆盖(核心安全) - -### JwtTransformMiddlewareTests - -| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 | -|---------|------|------|----------|-----------| -| `ShouldValidateJwtSignature` | 应验证 JWT 签名 | 有效签名的 JWT | 解析成功,Claims 正确 | `IOptions` | -| `ShouldRejectInvalidToken` | 应拒绝无效 Token | 伪造/过期 JWT | 返回 401 或跳过处理 | - | -| `ShouldExtractTenantClaim` | 应正确提取租户 ID | 含 tenant claim 的 JWT | X-Tenant-Id header 设置正确 | - | -| `ShouldHandleMissingToken` | 应处理无 Token 请求 | 无 Authorization header | 继续处理(不设置 headers) | - | -| `ShouldHandleMalformedToken` | 应处理格式错误 Token | 无效 JWT 格式 | 记录警告,继续处理 | - | - -### TenantRoutingMiddlewareTests - -| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 | -|---------|------|------|----------|-----------| -| `ShouldValidateTenantIdAgainstJwt` | 应验证 header 与 JWT 一致 | X-Tenant-Id ≠ JWT tenant | 返回 403 Forbidden | `IRouteCache` | -| `ShouldExtractServiceNameFromPath` | 应正确解析服务名 | `/api/user-service/users` | serviceName = "user-service" | - | -| `ShouldFindRouteInCache` | 应从缓存找到路由 | 有效租户+服务名 | 设置正确的 clusterId | `IRouteCache` | -| `ShouldHandleRouteNotFound` | 应处理路由未找到 | 不存在的服务名 | 记录警告,继续处理 | - | -| `ShouldPrioritizeTenantRouteOverGlobal` | 租户路由优先于全局 | 同时存在两种路由 | 使用租户路由 | - | - ---- - -## P1 - 重要覆盖(核心业务) - -### RouteCacheTests - -| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 | -|---------|------|------|----------|-----------| -| `ShouldLoadGlobalRoutes` | 应加载全局路由 | 全局路由数据 | `_globalRoutes` 填充 | `IDbContextFactory` | -| `ShouldLoadTenantRoutes` | 应加载租户路由 | 租户路由数据 | `_tenantRoutes` 填充 | - | -| `ShouldReturnCorrectRoute` | 应返回正确路由 | 查询请求 | 正确的 `RouteInfo` | - | -| `ShouldReturnNullForMissingRoute` | 不存在路由返回 null | 不存在的服务名 | `null` | - | -| `ShouldHandleConcurrentReads` | 并发读取应安全 | 多线程读取 | 无异常,数据一致 | - | -| `ShouldReloadCorrectly` | 应正确重载 | Reload 调用 | 旧数据清除,新数据加载 | - | - -### RedisConnectionManagerTests - -| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 | -|---------|------|------|----------|-----------| -| `ShouldAcquireLock` | 应获取分布式锁 | 有效 key | 锁获取成功 | `IConnectionMultiplexer` | -| `ShouldReleaseLockCorrectly` | 应正确释放锁 | 已获取的锁 | 锁释放成功 | - | -| `ShouldNotReleaseOthersLock` | 不应释放他人锁 | 其他实例的锁 | 释放失败(安全) | - | -| `ShouldHandleConnectionFailure` | 应处理连接失败 | Redis 不可用 | 记录错误,返回失败 | - | -| `ShouldExecuteInLock` | 应在锁内执行操作 | 操作委托 | 操作执行,锁正确释放 | - | - -### DistributedWeightedRoundRobinPolicyTests - -| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 | -|---------|------|------|----------|-----------| -| `ShouldSelectByWeight` | 应按权重选择 | 权重 [3, 1, 1] | 约 60% 选第一个 | `IConnectionMultiplexer` | -| `ShouldFallbackOnLockFailure` | 锁失败应降级 | Redis 不可用 | 降级选择第一个可用 | - | -| `ShouldReturnNullWhenNoDestinations` | 无目标返回 null | 空目标列表 | `null` | - | -| `ShouldPersistStateToRedis` | 状态应持久化到 Redis | 多次选择 | 状态存储正确 | - | -| `ShouldExpireStateAfterTTL` | 状态应在 TTL 后过期 | 1 小时后 | 状态重新初始化 | - | - -### PgSqlConfigChangeListenerTests - -| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 | -|---------|------|------|----------|-----------| -| `ShouldListenForNotifications` | 应监听通知 | NOTIFY 事件 | 触发重载 | `NpgsqlConnection` | -| `ShouldFallbackToPolling` | 应回退到轮询 | 通知失败 | 定时轮询检测 | - | -| `ShouldReconnectOnFailure` | 失败应重连 | 连接断开 | 自动重连 | - | -| `ShouldDetectVersionChange` | 应检测版本变化 | 版本号增加 | 触发重载 | - | - ---- - -## P2 - 推荐覆盖(业务逻辑) - -### GatewayConfigControllerTests - -| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 | -|---------|------|------|----------|-----------| -| `ShouldCreateTenant` | 应创建租户 | 有效 DTO | 201 Created | `IDbContextFactory` | -| `ShouldRejectDuplicateTenant` | 应拒绝重复租户 | 已存在的 TenantCode | 400 BadRequest | - | -| `ShouldCreateRoute` | 应创建路由 | 有效 DTO | 201 Created | - | -| `ShouldDeleteTenant` | 应删除租户 | 有效 ID | 204 NoContent | - | -| `ShouldReturn404ForMissingTenant` | 不存在租户返回 404 | 无效 ID | 404 NotFound | - | -| `ShouldReloadConfig` | 应重载配置 | POST /config/reload | 200 OK | `IRouteCache` | - -### PendingServicesControllerTests - -| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 | -|---------|------|------|----------|-----------| -| `ShouldListPendingServices` | 应列出待处理服务 | GET 请求 | 待处理服务列表 | `IDbContextFactory` | -| `ShouldAssignService` | 应分配服务 | 有效请求 | 服务实例创建 | - | -| `ShouldRejectInvalidCluster` | 应拒绝无效集群 | 不存在的 ClusterId | 400 BadRequest | - | -| `ShouldRejectService` | 应拒绝服务 | reject 请求 | 状态更新为 Rejected | - | - ---- - -## 测试依赖 - -```xml - - - - - - - - - - - -``` - ---- - -## 运行测试命令 - -```bash -# 运行所有测试 -dotnet test - -# 运行特定测试类 -dotnet test --filter "FullyQualifiedName~JwtTransformMiddlewareTests" - -# 生成覆盖率报告 -dotnet test --collect:"XPlat Code Coverage" -reportgenerator -reports:**/coverage.cobertura.xml -targetdir:coverage -``` - ---- - -## 覆盖率目标 - -| 组件 | 目标覆盖率 | 优先级 | -|------|-----------|--------| -| JwtTransformMiddleware | 90% | P0 | -| TenantRoutingMiddleware | 85% | P0 | -| RouteCache | 80% | P1 | -| DistributedWeightedRoundRobinPolicy | 80% | P1 | -| Controllers | 70% | P2 | -| 整体项目 | 75% | - | - ---- - -*测试计划由分析生成,建议按优先级逐步实现。* \ No newline at end of file diff --git a/.planning/config.json b/.planning/config.json deleted file mode 100644 index 8014a86..0000000 --- a/.planning/config.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "mode": "yolo", - "depth": "standard", - "parallelization": true, - "commit_docs": true, - "model_profile": "balanced", - "workflow": { - "research": false, - "plan_check": false, - "verifier": false, - "auto_advance": true - } -} diff --git a/.planning/gateway-plugin-system.md b/.planning/gateway-plugin-system.md deleted file mode 100644 index b8b895c..0000000 --- a/.planning/gateway-plugin-system.md +++ /dev/null @@ -1,712 +0,0 @@ -# 网关插件系统技术方案 - -## 一、概述 - -本文档描述 YARP 网关的插件系统规划,包括 Web UI 管理界面和动态编译加载两大核心功能。 - ---- - -## 二、整体架构 - -``` -┌─────────────────────┐ -│ fengling-console │ (运维后端 - Backend) -│ web 前端 │ -└─────────┬───────────┘ - │ HTTP API - ▼ -┌─────────────────────┐ -│ fengling-console │ (运维服务端) -│ │ -│ - 路由管理 API │ ───▶ 数据库持久化 -│ - 集群管理 API │ ───▶ Redis Pub/Sub (发布事件) -│ - 插件管理 API │ -└─────────┬───────────┘ - ▲ - │ 事件订阅 - │ -┌─────────┴───────────┐ -│ fengling-gateway │ (YARP 网关多实例) -│ - YARP 代理 │ -│ - 插件执行 │ -│ - 事件监听 │ -└─────────────────────┘ -``` - -### 项目职责 - -| 项目 | 职责 | -|------|------| -| **fengling-gateway** | 纯 YARP 代理 + 事件订阅 + 插件执行 | -| **fengling-console** | 运维 API + 配置持久化 + 事件发布 | -| **fengling-console-web** | 前端 UI (Monaco Editor) | - ---- - -## 三、Web UI 管理界面 - -### 3.1 技术选型 - -| 项目 | 选择 | 理由 | -|------|------|------| -| 前端框架 | React/Vue | 独立前端项目 | -| 编辑器 | Monaco Editor | VS Code 同款,体验一致 | -| 路由 | `/gateway` | 运维平台内统一路由 | - -### 3.2 功能模块 - -``` -/gateway -├── 路由管理 (Routes) -│ ├── 列表/搜索 -│ ├── 创建/编辑/删除 -│ └── 路由规则配置 -├── 集群管理 (Clusters) -│ ├── 上下游服务列表 -│ ├── 实例管理 -│ └── 健康状态 -├── 插件管理 (Plugins) -│ ├── 已加载插件列表 -│ ├── 上传 DLL -│ └── 在线编写 C# 代码 -└── 监控统计 - ├── QPS/延迟 - └── 流量图表 -``` - -## 一、概述 - -本文档描述 YARP 网关的插件系统规划,包括 Web UI 管理界面和动态编译加载两大核心功能。 - ---- - -## 二、Web UI 管理界面 - -### 2.1 技术选型 - -| 项目 | 选择 | 理由 | -|------|------|------| -| 框架 | Razor Pages | 嵌入主应用,单项目部署 | -| 路由 | `/gateway/ui` | 参考 SwaggerUI 风格 | -| 编辑器 | Monaco Editor | VS Code 同款,体验一致 | - -### 2.2 功能模块 - -``` -/gateway/ui -├── 路由管理 (Routes) -│ ├── 列表/搜索 -│ ├── 创建/编辑/删除 -│ └── 路由规则配置 -├── 集群管理 (Clusters) -│ ├── 上下游服务列表 -│ ├── 实例管理 -│ └── 健康状态 -├── 插件管理 (Plugins) -│ ├── 已加载插件列表 -│ ├── 上传 DLL -│ └── 在线编写 C# 代码 -└── 监控统计 - ├── QPS/延迟 - └── 流量图表 -``` - ---- - -## 三、插件系统架构 - -### 3.1 插件类型定义 - -```csharp -namespace Fengling.Gateway.Plugin.Abstractions -{ - /// - /// 插件基础接口 - /// - public interface IGatewayPlugin - { - string Name { get; } - string Version { get; } - string? Description { get; } - - Task OnLoadAsync(); - Task OnUnloadAsync(); - } - - /// - /// 请求处理插件 - /// - public interface IRequestPlugin : IGatewayPlugin - { - /// 请求到达网关前 - Task OnRequestAsync(HttpContext context); - - /// 路由决策后 - Task OnRouteMatchedAsync(HttpContext context, RouteConfig route); - - /// 转发到后端前 - Task OnForwardingAsync(HttpContext context, HttpRequestMessage request); - } - - /// - /// 响应处理插件 - /// - public interface IResponsePlugin : IGatewayPlugin - { - /// 后端响应后 - Task OnBackendResponseAsync(HttpContext context, HttpResponseMessage response); - - /// 返回客户端前 - Task OnResponseFinalizingAsync(HttpContext context); - } - - /// - /// 路由转换插件 - /// - public interface IRouteTransformPlugin : IGatewayPlugin - { - Task TransformRouteAsync(RouteConfig original, HttpContext context); - } - - /// - /// 负载均衡插件 - /// - public interface ILoadBalancePlugin : IGatewayPlugin - { - Task SelectDestinationAsync( - IReadOnlyList destinations, - HttpContext context); - } -} -``` - -### 3.2 插件阶段枚举 - -```csharp -public enum PipelineStage -{ - None = 0, - OnRequest = 1, // 请求到达网关前 - OnRoute = 2, // 路由决策时 - OnRequestBackend = 3, // 转发到后端前 - OnResponseBackend = 4, // 后端响应后 - OnResponse = 5 // 返回给客户端前 -} -``` - ---- - -## 四、核心模块设计 - -### 4.1 依赖管理(A方案) - -#### 简单场景:直接使用网关已有程序集 - -```csharp -// API 暴露网关程序集 -[ApiController] -public class AssembliesController : ControllerBase -{ - [HttpGet("available")] - public List GetAvailableAssemblies() - { - return AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)) - .Select(a => new AssemblyInfo - { - Name = a.GetName().Name, - Version = a.GetName().Version?.ToString(), - Location = a.Location - }) - .OrderBy(a => a.Name) - .ToList(); - } -} -``` - -#### 复杂场景:上传 ZIP 包 - -```csharp -public class PluginUploadService -{ - private readonly IObjectStorage _storage; - private readonly PluginDbContext _db; - private readonly string _localPluginPath = "/app/plugins"; - - public async Task UploadPluginAsync(IFormFile zipFile, string pluginName) - { - // 1. 上传到对象存储 - var storageKey = $"plugins/{Guid.NewGuid()}/{zipFile.FileName}"; - await _storage.UploadAsync(zipFile.OpenReadStream(), storageKey); - - // 2. 保存到数据库 - var plugin = new PluginPackage - { - Id = Guid.NewGuid(), - Name = pluginName, - StorageKey = storageKey, - UploadedAt = DateTime.UtcNow, - Status = PluginStatus.Pending - }; - - await _db.PluginPackages.AddAsync(plugin); - await _db.SaveChangesAsync(); - - return plugin; - } - - public async Task ExtractAndLoadAsync(Guid pluginId) - { - var plugin = await _db.PluginPackages.FindAsync(pluginId); - - // 3. 下载到本地 - var localDir = Path.Combine(_localPluginPath, plugin.Id.ToString()); - Directory.CreateDirectory(localDir); - - await _storage.DownloadAsync(plugin.StorageKey, localDir + ".zip"); - - // 4. 解压 - ZipFile.ExtractToDirectory(localDir + ".zip", localDir, overwriteFiles: true); - File.Delete(localDir + ".zip"); - - // 5. 加载插件 - await _pluginManager.LoadFromDirectoryAsync(localDir); - } -} -``` - -#### ZIP 上传验证 - -```csharp -public class PluginValidationService -{ - public async Task ValidateAsync(Stream zipStream) - { - var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - await ExtractZipAsync(zipStream, tempDir); - - try - { - var dlls = Directory.GetFiles(tempDir, "*.dll"); - var result = new PluginValidationResult(); - - foreach (var dll in dlls) - { - var dllResult = await ValidateDllAsync(dll); - result.Assemblies.Add(dllResult); - } - - var validPlugins = result.Assemblies - .Where(a => a.IsValidPlugin) - .ToList(); - - if (validPlugins.Count == 0) - { - result.IsValid = false; - result.ErrorMessage = "未找到实现 IGatewayPlugin 接口的类"; - } - else - { - result.IsValid = true; - result.ValidPluginTypes = validPlugins - .SelectMany(a => a.PluginTypes) - .ToList(); - } - - return result; - } - finally - { - Directory.Delete(tempDir, true); - } - } - - private async Task ValidateDllAsync(string dllPath) - { - var result = new DllValidationResult { DllName = Path.GetFileName(dllPath) }; - - try - { - var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(dllPath); - - var pluginTypes = assembly.GetTypes() - .Where(t => typeof(IGatewayPlugin).IsAssignableFrom(t)) - .Where(t => !t.IsAbstract && !t.IsInterface) - .ToList(); - - if (pluginTypes.Count > 0) - { - result.IsValidPlugin = true; - result.PluginTypes = pluginTypes.Select(t => new PluginTypeInfo - { - TypeName = t.FullName, - ImplementedInterfaces = t.GetInterfaces().Select(i => i.Name).ToList() - }).ToList(); - } - } - catch (Exception ex) - { - result.IsValid = false; - result.ErrorMessage = ex.Message; - } - - return result; - } -} -``` - ---- - -### 4.2 插件间通信(B方案) - -采用**方案 3:强类型 + 弱类型混合** - -#### 插件上下文 - -```csharp -public class GatewayContext -{ - // 预定义常用字段(强类型) - public string? UserId { get; set; } - public string? TenantId { get; set; } - public UserTier Tier { get; set; } - public bool IsAuthenticated { get; set; } - public DateTime RequestTime { get; set; } - - // 扩展数据(弱类型) - public PluginDataBag Data { get; } = new(); -} - -public class PluginDataBag -{ - private readonly Dictionary _data = new(); - - public T? Get(string key) => _data.TryGetValue(key, out var v) ? (T)v : default; - public void Set(string key, T value) => _data[key] = value!; -} -``` - -#### 使用示例 - -```csharp -// 插件 A: 认证 -public class AuthPlugin : IRequestPlugin -{ - public async Task OnRequestAsync(HttpContext context) - { - var userId = ValidateToken(context); - - if (userId != null) - { - context.Items["CurrentUserId"] = userId; - context.Items["IsAuthenticated"] = true; - } - - return context; - } -} - -// 插件 B: 审计 -public class AuditPlugin : IRequestPlugin -{ - public async Task OnRequestAsync(HttpContext context) - { - if (context.Items.TryGetValue("CurrentUserId", out var userId)) - { - await _logger.LogAsync($"User {userId} accessed {context.Request.Path}"); - } - - return context; - } -} -``` - ---- - -### 4.3 在线代码编辑器(C方案) - -采用 **Monaco Editor + 前端模拟补全** - -> **补充说明**:如需更完整的 C# IntelliSense(如真实代码分析、跳转到定义),可使用 Microsoft 官方的 **roslyn-language-server**(VS Code C# 扩展背后使用的语言服务器)。 -> -> **部署方式**: -> ```bash -> # 安装 -> dotnet tool install -g Microsoft.CodeAnalysis.LanguageServer -> -> # 启动服务 -> roslyn-languageserver --port 5000 -> ``` -> -> 前端通过 WebSocket 连接该服务获取完整的语言特性支持。但目前阶段前端模拟补全已足够使用。 - - - -```html - - -
- - -``` - -#### 编辑器模板 - -```csharp -// 生成的代码模板 -$@" -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Fengling.Gateway.Plugin.Abstractions; - -public class {pluginName} : IRequestPlugin -{{ - public string Name => ""{pluginName}""; - public string Version => ""1.0.0""; - - public async Task OnRequestAsync(HttpContext ctx) - {{ - // 编写你的逻辑 - {userCode} - }} -}} -"; -``` - ---- - -### 4.4 插件生命周期管理 - -```csharp -public class PluginManager -{ - private readonly Dictionary _plugins = new(); - private readonly RoslynPluginCompiler _compiler = new(); - - public async Task LoadPluginAsync(byte[] assemblyBytes, string pluginName) - { - // 1. 隔离加载程序集 - var context = new PluginLoadContext(pluginName); - var assembly = context.LoadFromStream(new MemoryStream(assemblyBytes)); - - // 2. 查找插件入口类型 - var pluginType = assembly.GetTypes() - .FirstOrDefault(t => typeof(IGatewayPlugin).IsAssignableFrom(t)); - - if (pluginType == null) - { - context.Unload(); - throw new InvalidOperationException("No IGatewayPlugin implementation found"); - } - - // 3. 创建实例 - var plugin = (IGatewayPlugin)Activator.CreateInstance(pluginType)!; - await plugin.OnLoadAsync(); - - // 4. 保存实例 - var instance = new PluginInstance - { - Name = pluginName, - Assembly = assembly, - Context = context, - Plugin = plugin, - LoadedAt = DateTime.UtcNow - }; - - _plugins[pluginName] = instance; - return instance; - } - - public async Task UnloadPluginAsync(string pluginName) - { - if (!_plugins.TryGetValue(pluginName, out var instance)) - return; - - await instance.Plugin.OnUnloadAsync(); - instance.Context.Unload(); - _plugins.Remove(pluginName); - } -} - -public class PluginLoadContext : AssemblyLoadContext -{ - public PluginLoadContext(string name) : base(name, isCollectible: true) { } - - protected override Assembly? Load(AssemblyName assemblyName) - { - return null; - } -} -``` - ---- - -### 4.5 插件编译服务 - -```csharp -public class RoslynPluginCompiler -{ - private readonly IEnumerable _defaultReferences; - - public RoslynPluginCompiler() - { - _defaultReferences = GetDefaultReferences(); - } - - public CompileResult Compile(string sourceCode, string pluginName, IEnumerable extraAssemblies) - { - var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode); - - var references = _defaultReferences.Concat( - extraAssemblies.Select(a => MetadataReference.CreateFromFile(a)) - ); - - var compilation = CSharpCompilation.Create( - assemblyName: $"Plugin_{pluginName}_{Guid.NewGuid():N}", - syntaxTrees: new[] { syntaxTree }, - references: references, - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) - .WithAllowUnsafe(false) - .WithOptimizationLevel(OptimizationLevel.Release)); - - using var ms = new MemoryStream(); - var emitResult = compilation.Emit(ms); - - if (!emitResult.Success) - { - return CompileResult.Fail(emitResult.Diagnostics); - } - - ms.Seek(0, SeekOrigin.Begin); - return CompileResult.Success(ms.ToArray()); - } - - private IEnumerable GetDefaultReferences() - { - return AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)) - .Select(a => MetadataReference.CreateFromFile(a.Location)); - } -} -``` - ---- - -## 五、数据库模型 - -```csharp -public class PluginPackage -{ - public Guid Id { get; set; } - public string Name { get; set; } = string.Empty; - public string Version { get; set; } = "1.0.0"; - public string? Description { get; set; } - public string StorageKey { get; set; } = string.Empty; - public PluginStatus Status { get; set; } - public DateTime UploadedAt { get; set; } - public DateTime? LoadedAt { get; set; } -} - -public class PluginPipeline -{ - public Guid Id { get; set; } - public Guid PluginId { get; set; } - public PipelineStage Stage { get; set; } - public int Order { get; set; } - public bool IsEnabled { get; set; } -} - -public enum PluginStatus -{ - Pending = 0, - Validated = 1, - Loaded = 2, - Failed = 3, - Disabled = 4 -} -``` - ---- - -## 六、实施计划 - -| 阶段 | 任务 | 优先级 | -|------|------|--------| -| Phase 1 | 集成 YARP 到现有项目 | 高 | -| Phase 2 | 插件基础接口定义 | 高 | -| Phase 3 | 插件编译 + 加载框架 | 高 | -| Phase 4 | 嵌入式 Razor UI | 中 | -| Phase 5 | Monaco Editor 集成 | 中 | -| Phase 6 | ZIP 上传验证功能 | 中 | -| Phase 7 | 测试与优化 | 低 | - ---- - -## 七、NuGet 依赖 - -```xml - - - - - - - - -``` - ---- - -## 八、总结 - -本方案实现了: - -1. **Web UI 管理**:类 SwaggerUI 风格的可视化界面 -2. **动态编译**:Roslyn 在线编译 C# 代码 -3. **插件加载**:独立 AssemblyLoadContext,支持热卸载 -4. **灵活扩展**:支持简单场景(使用已有程序集)和复杂场景(上传 ZIP) -5. **流程控制**:插件可分配到 5 个不同阶段执行 - ---- - -*文档版本: 1.0* -*最后更新: 2026-03-01* diff --git a/.planning/phases/006-gateway-plugin-research/006-01-PLAN.md b/.planning/phases/006-gateway-plugin-research/006-01-PLAN.md deleted file mode 100644 index f67b984..0000000 --- a/.planning/phases/006-gateway-plugin-research/006-01-PLAN.md +++ /dev/null @@ -1,290 +0,0 @@ ---- -phase: 06-gateway-plugin-research -plan: 01 -type: execute -wave: 1 -depends_on: [] -files_modified: [] -autonomous: true -requirements: [PLUG-01, PLUG-02] -must_haves: - truths: - - "插件可以从指定目录动态加载" - - "插件在独立的 AssemblyLoadContext 中运行" - - "插件可以被卸载并释放内存" - artifacts: - - path: "src/yarpgateway/Plugins/PluginLoadContext.cs" - provides: "ALC 隔离机制" - - path: "src/yarpgateway/Plugins/PluginLoader.cs" - provides: "插件发现和加载" - - path: "src/yarpgateway/Plugins/PluginHost.cs" - provides: "插件生命周期管理" - - path: "tests/YarpGateway.Tests/Unit/Plugins/PluginLoadTests.cs" - provides: "加载/卸载验证" - key_links: - - from: "PluginLoader" - to: "PluginLoadContext" - via: "实例化并加载程序集" - - from: "PluginHost" - to: "PluginLoader" - via: "协调插件生命周期" ---- - -# 计划 01:插件加载基础设施 - - -实现插件动态加载基础设施,包括 AssemblyLoadContext 隔离、插件发现和生命周期管理。 - -**目的:** 为网关提供安全的插件加载机制,支持隔离和热重载。 -**产出:** 可工作的插件加载系统,含单元测试。 - - - -@/Users/mac/.config/opencode/get-shit-done/workflows/execute-plan.md -@/Users/mac/.config/opencode/get-shit-done/templates/summary.md - - - -@.planning/PROJECT.md -@.planning/ROADMAP.md -@.planning/STATE.md -@.planning/phases/006-gateway-plugin-research/006-RESEARCH.md - -# 现有基础设施 -@src/Fengling.Gateway.Plugin.Abstractions/IGatewayPlugin.cs -@src/yarpgateway/Program.cs - - - - -```csharp -// Fengling.Gateway.Plugin.Abstractions -public interface IGatewayPlugin -{ - string Name { get; } - string Version { get; } - string? Description { get; } - Task OnLoadAsync(); - Task OnUnloadAsync(); -} -``` - - - - - - Task 1: 创建 PluginLoadContext 隔离机制 - - src/yarpgateway/Plugins/PluginLoadContext.cs, - tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs - - - - Test 1: 加载插件程序集到独立 ALC - - Test 2: 共享契约程序集使用默认 ALC - - Test 3: 卸载 ALC 后内存被回收 - - -创建可卸载的 AssemblyLoadContext: - -1. 创建 `src/yarpgateway/Plugins/PluginLoadContext.cs` -2. 继承 AssemblyLoadContext,设置 isCollectible: true -3. 使用 AssemblyDependencyResolver 解析依赖 -4. 关键:共享 `Fengling.Gateway.Plugin.Abstractions` 到默认 ALC - -```csharp -public sealed class PluginLoadContext : AssemblyLoadContext -{ - private readonly AssemblyDependencyResolver _resolver; - private readonly string _sharedAssemblyName = "Fengling.Gateway.Plugin.Abstractions"; - - public PluginLoadContext(string pluginPath) : base(isCollectible: true) - { - _resolver = new AssemblyDependencyResolver(pluginPath); - } - - protected override Assembly? Load(AssemblyName assemblyName) - { - if (assemblyName.Name == _sharedAssemblyName) - return null; // 使用默认 ALC - var path = _resolver.ResolveAssemblyToPath(assemblyName); - return path != null ? LoadFromAssemblyPath(path) : null; - } - - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) - { - var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); - return path != null ? LoadUnmanagedDllFromPath(path) : IntPtr.Zero; - } -} -``` - -**注意**:先写测试,确保卸载验证通过。 - - - dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoadContextTests" --no-build - - - - PluginLoadContext 类存在 - - 测试验证隔离和卸载 - - 构建通过 - - - - - Task 2: 创建 PluginLoader 发现和加载逻辑 - - src/yarpgateway/Plugins/PluginLoader.cs, - src/yarpgateway/Plugins/DiscoveredPlugin.cs, - tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs - - - - Test 1: 从目录发现插件程序集 - - Test 2: 加载插件并返回 IGatewayPlugin 实例 - - Test 3: 处理无效插件(返回 null 或异常) - - -创建插件发现和加载器: - -1. 创建 `DiscoveredPlugin.cs` 记录: - - Id, Name, Version, AssemblyPath, EntryPoint - -2. 创建 `PluginLoader.cs`: - - `DiscoverPlugins(string directory)` - 扫描目录发现插件 - - `LoadPlugin(DiscoveredPlugin discovered)` - 加载并实例化 - - 使用 System.Reflection.Metadata 或简单扫描 - -```csharp -public class PluginLoader -{ - public IEnumerable DiscoverPlugins(string pluginDirectory) - { - // 扫描 plugin.json 或程序集属性 - } - - public IGatewayPlugin? LoadPlugin(DiscoveredPlugin discovered) - { - var shadowPath = CreateShadowCopy(discovered.AssemblyPath); - var alc = new PluginLoadContext(shadowPath); - var assembly = alc.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(discovered.AssemblyPath))); - var type = assembly.GetType(discovered.EntryPoint); - return Activator.CreateInstance(type) as IGatewayPlugin; - } -} -``` - -**影子复制**:创建 `CreateShadowCopy` 方法,将插件复制到临时目录。 - - - dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoaderTests" --no-build - - - - PluginLoader 类存在 - - 可发现和加载插件 - - 测试通过 - - - - - Task 3: 创建 PluginHost 生命周期管理 - - src/yarpgateway/Plugins/PluginHost.cs, - src/yarpgateway/Plugins/PluginHandle.cs, - tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs - - - - Test 1: LoadAllAsync 加载目录下所有插件 - - Test 2: UnloadAsync 卸载指定插件 - - Test 3: GetPlugins 返回当前加载的插件 - - Test 4: 插件卸载后 WeakReference 显示已回收 - - -创建插件生命周期管理器: - -1. 创建 `PluginHandle.cs`: - - 封装 PluginLoadContext 和 IGatewayPlugin - - 实现 IAsyncDisposable - - 提供 TrackUnloadability() 返回 WeakReference - -```csharp -public sealed class PluginHandle : IAsyncDisposable -{ - private readonly PluginLoadContext _alc; - private readonly IGatewayPlugin _plugin; - private readonly string _shadowDirectory; - private bool _disposed; - - public IGatewayPlugin Plugin => _plugin; - public WeakReference TrackUnloadability() => new(_alc, trackResurrection: true); - - public async ValueTask DisposeAsync() - { - if (_disposed) return; - _disposed = true; - await _plugin.OnUnloadAsync(); - _alc.Unload(); - } -} -``` - -2. 创建 `PluginHost.cs`: - - 注入 PluginLoader - - 管理插件字典 (name -> PluginHandle) - - 提供 LoadAllAsync, UnloadAsync, GetPlugins - -```csharp -public class PluginHost -{ - private readonly ConcurrentDictionary _plugins = new(); - private readonly PluginLoader _loader; - - public async Task LoadAllAsync(string pluginDirectory, CancellationToken ct = default) - { - var discovered = _loader.DiscoverPlugins(pluginDirectory); - foreach (var d in discovered) - { - var handle = _loader.LoadPlugin(d); - if (handle != null) - { - await handle.Plugin.OnLoadAsync(); - _plugins[d.Id] = handle; - } - } - } - - public async Task UnloadAsync(string pluginId) - { - if (_plugins.TryRemove(pluginId, out var handle)) - { - await handle.DisposeAsync(); - } - } -} -``` - - - dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginHostTests" --no-build - - - - PluginHost 和 PluginHandle 类存在 - - 可加载/卸载插件 - - 卸载验证测试通过 - - - - - - -1. dotnet build src/yarpgateway/YarpGateway.csproj 无错误 -2. dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoad" 通过 -3. 插件加载/卸载功能可用 - - - -- 插件可在独立 AssemblyLoadContext 中加载 -- 插件可通过 WeakReference 验证卸载 -- 所有单元测试通过 - - - -完成后创建 `.planning/phases/006-gateway-plugin-research/006-01-SUMMARY.md` - \ No newline at end of file diff --git a/.planning/phases/006-gateway-plugin-research/006-01-SUMMARY.md b/.planning/phases/006-gateway-plugin-research/006-01-SUMMARY.md deleted file mode 100644 index 72d7f40..0000000 --- a/.planning/phases/006-gateway-plugin-research/006-01-SUMMARY.md +++ /dev/null @@ -1,57 +0,0 @@ -# 计划 006-01 总结:插件加载基础设施 - -## 执行状态:✅ 已完成 - -## 完成的任务 - -### Task 1: PluginLoadContext 隔离机制 -- ✅ 创建 `src/yarpgateway/Plugins/PluginLoadContext.cs` -- ✅ 实现可卸载的 AssemblyLoadContext -- ✅ 支持共享契约程序集(使用默认 ALC) -- ✅ 创建单元测试 `PluginLoadContextTests.cs` - -### Task 2: PluginLoader 发现和加载逻辑 -- ✅ 创建 `src/yarpgateway/Plugins/PluginLoader.cs` -- ✅ 实现插件发现(从目录扫描 plugin.json) -- ✅ 实现影子复制(支持热重载) -- ✅ 创建 `DiscoveredPlugin.cs` 和 `PluginMetadata.cs` -- ✅ 创建单元测试 `PluginLoaderTests.cs` - -### Task 3: PluginHost 生命周期管理 -- ✅ 创建 `src/yarpgateway/Plugins/PluginHost.cs` -- ✅ 实现 `PluginHandle.cs` 封装 ALC 和插件实例 -- ✅ 支持加载/卸载/重载插件 -- ✅ 提供 WeakReference 卸载验证 -- ✅ 创建单元测试 `PluginHostTests.cs` - -## 实现的文件 - -| 文件 | 描述 | -|------|------| -| `src/yarpgateway/Plugins/PluginLoadContext.cs` | ALC 隔离机制 | -| `src/yarpgateway/Plugins/PluginLoader.cs` | 插件发现和加载 | -| `src/yarpgateway/Plugins/PluginHost.cs` | 生命周期管理 | -| `tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs` | 隔离测试 | -| `tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs` | 加载测试 | -| `tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs` | 生命周期测试 | - -## 测试结果 - -``` -dotnet test --filter "FullyQualifiedName~Plugin" -已通过! - 失败: 0,通过: 15,总计: 15 -``` - -## 验证 - -- ✅ 构建通过:`dotnet build src/yarpgateway/YarpGateway.csproj` - 0 错误 -- ✅ 所有单元测试通过 -- ✅ 插件可在独立 AssemblyLoadContext 中加载 -- ✅ 插件可通过 WeakReference 验证卸载 -- ✅ 支持动态发现和加载插件 - -## 后续计划 - -阶段 6 还需完成: -- PLUG-03:插件隔离与生命周期管理(已实现基础设施) -- YARP 集成:将插件集成到 YARP 管道 diff --git a/.planning/phases/006-gateway-plugin-research/006-02-PLAN.md b/.planning/phases/006-gateway-plugin-research/006-02-PLAN.md deleted file mode 100644 index 8be44c8..0000000 --- a/.planning/phases/006-gateway-plugin-research/006-02-PLAN.md +++ /dev/null @@ -1,366 +0,0 @@ ---- -phase: 06-gateway-plugin-research -plan: 02 -type: execute -wave: 1 -depends_on: [006-01] -files_modified: [] -autonomous: true -requirements: [PLUG-03] -must_haves: - truths: - - "插件通过路由 Metadata 启用" - - "请求 Transform 轻量处理请求" - - "目标选择在路由匹配后执行" - - "插件按配置顺序执行" - artifacts: - - path: "src/yarpgateway/Plugins/PluginTransformProvider.cs" - provides: "YARP Transform 提供者" - - path: "src/yarpgateway/Plugins/YarpPluginMiddleware.cs" - provides: "插件管道集成" - - path: "src/yarpgateway/Plugins/PluginConfigWatcher.cs" - provides: "Console DB 通知监听" - - path: "tests/YarpGateway.Tests/Unit/Plugins/YarpIntegrationTests.cs" - provides: "集成测试" -key_links: - - from: "PluginTransformProvider" - to: "PluginHost" - via: "获取已加载插件" - - from: "YarpPluginMiddleware" - to: "PluginTransformProvider" - via: "应用 Transform" - - from: "PluginConfigWatcher" - to: "PluginHost" - via: "触发重载" ---- - -# 计划 02:YARP 插件集成 - - -将插件系统集成到 YARP 反向代理管道,实现 Transform 方式的请求/响应处理,以及通过 Metadata 驱动的插件启用机制。 - -**目的:** 实现 PLUG-03 - 插件隔离与生命周期管理,完成网关插件化。 -**产出:** 可工作的 YARP 插件集成系统,含单元测试。 - - - -@/Users/mac/.config/opencode/get-shit-done/workflows/execute-plan.md -@/Users/mac/.config/opencode/get-shit-done/templates/summary.md - - - -@.planning/PROJECT.md -@.planning/ROADMAP.md -@.planning/STATE.md -@.planning/phases/006-gateway-plugin-research/006-RESEARCH.md -@src/Fengling.Gateway.Plugin.Abstractions/IGatewayPlugin.cs -@src/yarpgateway/Plugins/PluginHost.cs -@src/yarpgateway/Program.cs - - - -## 现有插件接口 - -```csharp -// Fengling.Gateway.Plugin.Abstractions -public interface IGatewayPlugin -{ - string Name { get; } - string Version { get; } - string? Description { get; } - Task OnLoadAsync(); - Task OnUnloadAsync(); -} - -public interface IRequestPlugin : IGatewayPlugin -{ - Task OnRequestAsync(HttpContext context); - Task OnRouteMatchedAsync(HttpContext context, RouteConfig route); - Task OnForwardingAsync(HttpContext context, HttpRequestMessage request); -} - -public interface IResponsePlugin : IGatewayPlugin -{ - Task OnBackendResponseAsync(HttpContext context, HttpResponseMessage response); - Task OnResponseFinalizingAsync(HttpContext context); -} - -public interface IRouteTransformPlugin : IGatewayPlugin -{ - Task TransformRouteAsync(RouteConfig original, HttpContext context); -} -``` - -## YARP Transform 接口 - -```csharp -// YARP Transform -public interface IRequestTransform -{ - Task ApplyAsync(RequestTransformContext context); -} - -public interface IResponseTransform -{ - Task ApplyAsync(ResponseTransformContext context); -} - -public interface IClusterDestinationsTransform -{ - Task ApplyAsync(ClusterDestinationsContext context); -} -``` - - - - - - Task 1: 创建 PluginTransformProvider - - src/yarpgateway/Plugins/PluginTransformProvider.cs, - tests/YarpGateway.Tests/Unit/Plugins/PluginTransformProviderTests.cs - - - - Test 1: 从路由 Metadata 发现启用的插件 - - Test 2: 按 PluginOrder 排序 - - Test 3: 创建 RequestTransform - - Test 4: 创建 ResponseTransform - - -创建 YARP Transform 提供者: - -1. 创建 `PluginTransformProvider.cs`: - - 实现 `IProxyConfigFilter` 或 `IDynamicRouteConfigProvider` - - 扫描路由 Metadata 中的 "Plugins" 键 - - 从 PluginHost 获取已加载的插件 - - 按 "PluginOrder" 排序 - -2. 核心逻辑: -```csharp -public class PluginTransformProvider : IProxyConfigFilter -{ - private readonly PluginHost _pluginHost; - - public async Task ApplyTransformAsync(HttpContext context, RouteConfig route) - { - var pluginIds = route.Metadata?["Plugins"]?.Split(','); - if (pluginIds == null) return; - - var order = route.Metadata?["PluginOrder"]?.Split(',') ?? pluginIds; - var orderedPlugins = order.Select(id => pluginIds.IndexOf(id)) - .Select(i => _pluginHost.GetPlugins().ElementAt(i)); - - foreach (var plugin in orderedPlugins.OfType()) - { - await plugin.OnRouteMatchedAsync(context, route); - } - } -} -``` - -3. 创建 Request/Response Transform 类: - - `PluginRequestTransform` - 调用 IRequestPlugin - - `PluginResponseTransform` - 调用 IResponsePlugin - - `PluginRouteTransform` - 调用 IRouteTransformPlugin - - - dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginTransformProviderTests" --no-build - - - - PluginTransformProvider 存在 - - Transform 测试通过 - - - - - Task 2: 创建目标选择器 (DestinationSelector) - - src/yarpgateway/Plugins/DestinationSelector.cs, - tests/YarpGateway.Tests/Unit/Plugins/DestinationSelectorTests.cs - - - - Test 1: OnRouteMatchedAsync 选择目标 - - Test 2: 根据上下文修改目标列表 - - Test 3: 特殊租户路由到特殊目标 - - -创建目标选择器,用于 OnRouteMatchedAsync 阶段: - -1. 创建 `DestinationSelector.cs`: - - 实现 IClusterDestinationsTransform - - 在路由匹配后、负载均衡前执行 - - 可以根据 HttpContext 修改可用目标列表 - -```csharp -public class DestinationSelector : IClusterDestinationsTransform -{ - private readonly PluginHost _pluginHost; - - public async Task ApplyAsync(ClusterDestinationsContext context) - { - var httpContext = context.HttpContext; - - // 获取路由上启用的插件 - var routeConfig = httpContext.GetRouteConfig(); - var pluginIds = routeConfig?.Metadata?["Plugins"]?.Split(','); - - if (pluginIds == null) return; - - foreach (var pluginId in pluginIds) - { - var plugin = _pluginHost.GetPlugin(pluginId); - if (plugin is IRequestPlugin requestPlugin) - { - // 让插件过滤/修改目标列表 - var availableDestinations = context.Destinations.ToList(); - // 插件逻辑可以修改 availableDestinations - context.Destinations = availableDestinations; - } - } - } -} -``` - -2. 注册到 YARP: -```csharp -builder.Services.AddSingleton(); -``` - - - dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~DestinationSelectorTests" --no-build - - - - DestinationSelector 存在 - - 目标选择逻辑工作正常 - - - - - Task 3: 创建 PluginConfigWatcher (Console DB 通知) - - src/yarpgateway/Plugins/PluginConfigWatcher.cs, - tests/YarpGateway.Tests/Unit/Plugins/PluginConfigWatcherTests.cs - - - - Test 1: 监听配置变更通知 - - Test 2: 触发插件重载 - - Test 3: 处理通知失败 - - -创建配置监听器,监听 Console DB 通知: - -1. 创建 `PluginConfigWatcher.cs`: - - 实现 `IHostedService` 或使用现有的 `PgSqlConfigChangeListener` - - 监听插件配置变更频道(如 `plugin_config_changed`) - - 触发 PluginHost 重载 - -```csharp -public class PluginConfigWatcher : BackgroundService -{ - private readonly PluginHost _pluginHost; - private readonly NpgsqlConnection _connection; - private const string Channel = "plugin_config_changed"; - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await _connection.OpenAsync(stoppingToken); - await _connection.ListenAsync(Channel, stoppingToken); - - await foreach (var notification in _connection.Notifications(stoppingToken)) - { - // 解析通知,获取需要重载的插件 - var payload = JsonSerializer.Deserialize(notification.Payload); - await _pluginHost.ReloadAsync(payload.PluginId); - } - } -} -``` - -2. 注册到 DI: -```csharp -builder.Services.AddHostedService(); -``` - - - dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginConfigWatcherTests" --no-build - - - - PluginConfigWatcher 存在 - - 监听器正确响应通知 - - - - - Task 4: 更新 Program.cs 集成插件系统 - - src/yarpgateway/Program.cs - - - - Test 1: 插件系统在启动时初始化 - - Test 2: Transform 被正确应用 - - -更新 Program.cs 集成插件系统: - -1. 注册插件服务: -```csharp -// 插件目录 -var pluginDirectory = configuration.GetValue("Plugin:Directory") ?? "plugins"; - -// 插件主机 -builder.Services.AddSingleton(sp => new PluginHost(pluginDirectory)); - -// Transform 提供者 -builder.Services.AddSingleton(); - -// 目标选择器 -builder.Services.AddSingleton(); - -// 配置监听器 -builder.Services.AddHostedService(); -``` - -2. 初始化插件: -```csharp -var app = builder.Build(); - -// 启动时加载插件 -var pluginHost = app.Services.GetRequiredService(); -await pluginHost.LoadAllAsync(); -``` - -3. 配置 YARP 使用 Transform: -```csharp -builder.Services.AddReverseProxy() - .AddTransforms(); -``` - - - dotnet build src/yarpgateway/YarpGateway.csproj - - - - Program.cs 正确集成插件系统 - - 构建通过 - - - - - - -1. dotnet build src/yarpgateway/YarpGateway.csproj 无错误 -2. dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~Plugin" 通过 -3. 插件可通过 Metadata 启用 -4. Transform 正确应用到请求/响应 - - - -- 插件通过 YARP Transform 管道处理请求 -- 目标选择在路由匹配后执行 -- 插件通过 Metadata 启用 -- Console DB 通知可触发插件重载 -- 所有单元测试通过 - - - -完成后创建 `.planning/phases/006-gateway-plugin-research/006-02-SUMMARY.md` - diff --git a/.planning/phases/006-gateway-plugin-research/006-RESEARCH.md b/.planning/phases/006-gateway-plugin-research/006-RESEARCH.md deleted file mode 100644 index 15e7afd..0000000 --- a/.planning/phases/006-gateway-plugin-research/006-RESEARCH.md +++ /dev/null @@ -1,284 +0,0 @@ -# 阶段 6 研究:网关插件技术调研与实现 - -**研究日期:** 2026-03-04 -**状态:** 已完成 - ---- - -## 1. 现有基础设施分析 - -### 1.1 已有插件抽象层 - -项目已创建 `Fengling.Gateway.Plugin.Abstractions` 程序集,定义了核心插件接口: - -```csharp -// 已定义的接口 -public interface IGatewayPlugin -{ - string Name { get; } - string Version { get; } - string? Description { get; } - Task OnLoadAsync(); - Task OnUnloadAsync(); -} - -public interface IRequestPlugin : IGatewayPlugin { ... } -public interface IResponsePlugin : IGatewayPlugin { ... } -public interface IRouteTransformPlugin : IGatewayPlugin { ... } -public interface ILoadBalancePlugin : IGatewayPlugin { ... } -``` - -### 1.2 缺失的组件 - -- ❌ **插件加载器** - 动态加载程序集的机制 -- ❌ **插件生命周期管理** - 加载/卸载/热重载 -- ❌ **插件隔离** - AssemblyLoadContext 隔离 -- ❌ **YARP 集成** - 将插件集成到 YARP 管道 - ---- - -## 2. YARP 扩展点 - -### 2.1 中间件管道 - -YARP 使用 ASP.NET Core 中间件管道,允许自定义注入点: - -```csharp -app.MapReverseProxy(proxyPipeline => -{ - proxyPipeline.Use(async (context, next) => - { - // 插件前置执行 - await pluginFeature.ExecutePreProxyAsync(context); - await next(); - // 插件后置执行 - await pluginFeature.ExecutePostProxyAsync(context); - }); -}); -``` - -### 2.2 Transform 管道(推荐) - -Transform 是修改请求/响应的推荐方式: - -```csharp -builder.Services.AddReverseProxy() - .AddTransforms(context => - { - var plugins = PluginManager.GetTransformPlugins(context.Route); - foreach (var plugin in plugins) - { - context.AddRequestTransform(plugin.ApplyAsync); - } - }); -``` - -### 2.3 路由扩展 - -YARP 2.0+ 支持自定义路由元数据,插件可消费: - -```json -{ - "Routes": { - "api-route": { - "Extensions": { - "PluginConfig": { - "PluginId": "rate-limiter", - "MaxRequests": 100 - } - } - } - } -} -``` - ---- - -## 3. .NET 插件加载最佳实践 - -### 3.1 AssemblyLoadContext 隔离 - -使用 **可卸载的 AssemblyLoadContext** 配合 **AssemblyDependencyResolver**: - -```csharp -public sealed class PluginLoadContext : AssemblyLoadContext -{ - private readonly AssemblyDependencyResolver _resolver; - private readonly string _sharedAssemblyName = "Fengling.Gateway.Plugin.Abstractions"; - - public PluginLoadContext(string pluginPath) : base(isCollectible: true) - { - _resolver = new AssemblyDependencyResolver(pluginPath); - } - - protected override Assembly? Load(AssemblyName assemblyName) - { - // 共享契约程序集(防止类型身份问题) - if (assemblyName.Name == _sharedAssemblyName) - return null; // 回退到默认 ALC - - var path = _resolver.ResolveAssemblyToPath(assemblyName); - return path != null ? LoadFromAssemblyPath(path) : null; - } -} -``` - -### 3.2 影子复制(热重载必需) - -Windows 锁定加载的 DLL,需要影子复制: - -```csharp -public static string CreateShadowCopy(string pluginDirectory) -{ - var shadowDir = Path.Combine( - Path.GetTempPath(), - "PluginShadows", - $"{Path.GetFileName(pluginDirectory)}_{Guid.NewGuid():N}" - ); - CopyDirectory(pluginDirectory, shadowDir); - return shadowDir; -} -``` - -### 3.3 插件句柄模式 - -```csharp -public sealed class PluginHandle : IAsyncDisposable -{ - private readonly PluginLoadContext _alc; - private readonly IGatewayPlugin _plugin; - - public async ValueTask DisposeAsync() - { - if (_plugin is IAsyncDisposable ad) await ad.DisposeAsync(); - _alc.Unload(); // 计划回收 - } -} -``` - -### 3.4 卸载验证 - -```csharp -public static bool VerifyUnload(WeakReference weakRef, int maxAttempts = 10) -{ - for (var i = 0; i < maxAttempts && weakRef.IsAlive; i++) - { - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - } - return !weakRef.IsAlive; -} -``` - ---- - -## 4. 插件隔离模式 - -### 4.1 契约边界(最重要的设计决策) - -**规则**:契约必须驻留在 **默认 ALC**,绝不在插件 ALC 中。 - -插件项目配置: -```xml - - false - runtime - -``` - -### 4.2 静态缓存问题 - -**问题**:主机单例缓存插件类型会阻止卸载。 - -**解决方案**:只使用 DTO 跨越边界,不传递插件类型。 - ---- - -## 5. 实现架构 - -### 5.1 组件结构 - -``` -src/ -├── Fengling.Gateway.Plugin.Abstractions/ # 已存在 -│ └── IGatewayPlugin.cs -├── yarpgateway/ -│ ├── Plugins/ -│ │ ├── PluginLoadContext.cs # ALC 隔离 -│ │ ├── PluginLoader.cs # 加载逻辑 -│ │ ├── PluginHost.cs # 生命周期管理 -│ │ └── PluginMiddleware.cs # YARP 集成 -│ └── Program.cs -└── plugins/ # 插件目录 - └── sample-plugin/ - └── SamplePlugin.csproj -``` - -### 5.2 插件目录结构 - -``` -plugins/ -├── rate-limiter/ -│ ├── RateLimiterPlugin.dll -│ ├── RateLimiterPlugin.deps.json -│ └── plugin.json # 元数据 -└── jwt-transform/ - └── ... -``` - -### 5.3 插件元数据 (plugin.json) - -```json -{ - "id": "rate-limiter", - "name": "Rate Limiter Plugin", - "version": "1.0.0", - "entryPoint": "RateLimiterPlugin.RateLimiterPlugin", - "interfaces": ["IRequestPlugin"], - "dependencies": [] -} -``` - ---- - -## 6. 验证架构 - -### 6.1 测试策略 - -1. **单元测试**:PluginLoadContext 隔离验证 -2. **集成测试**:插件加载/卸载/热重载 -3. **性能测试**:插件执行开销 - -### 6.2 成功标准验证 - -| 标准 | 验证方法 | -|------|---------| -| 动态加载插件 | 单元测试:从目录加载并执行 | -| 插件相互隔离 | 单元测试:异常不传播到其他插件 | -| 热加载/卸载 | 集成测试:WeakReference 验证卸载 | - ---- - -## 7. 推荐库 - -| 用途 | 库 | -|------|---| -| 网关核心 | Yarp.ReverseProxy 2.3.0+ | -| 插件加载 | 自定义 AssemblyLoadContext | -| 元数据读取 | System.Reflection.Metadata | -| 依赖注入 | Microsoft.Extensions.DependencyInjection | - ---- - -## 8. 关键注意事项 - -1. **不要在主机单例中缓存插件类型** -2. **始终用 WeakReference 测试卸载** -3. **Windows 上必须影子复制以支持热重载** -4. **使用仅元数据发现避免仅扫描而加载程序集** -5. **谨慎处理原生依赖**(它们不能干净卸载) - ---- - -*研究完成:2026-03-04* \ No newline at end of file diff --git a/.planning/phases/2-k8s-health-delegation/PLAN.md b/.planning/phases/2-k8s-health-delegation/PLAN.md deleted file mode 100644 index 678f2be..0000000 --- a/.planning/phases/2-k8s-health-delegation/PLAN.md +++ /dev/null @@ -1,71 +0,0 @@ -# 阶段 2 计划:K8s 健康检查委托 - -## 目标 - -将 K8s 服务健康监控从网关移除,委托给 fengling-console。网关只专注于请求路由。 - -## 需要移除的代码 - -### 1. 后台服务 -- **文件**: `src/yarpgateway/Services/KubernetesPendingSyncService.cs` -- **操作**: 删除文件 -- **影响**: 停止每 30 秒同步 K8s 服务 - -### 2. 服务注册 -- **文件**: `src/yarpgateway/Program.cs` -- **行号**: ~118 -- **代码**: `builder.Services.AddHostedService();` -- **操作**: 删除该行 - -### 3. API 控制器 -- **文件**: `src/yarpgateway/Controllers/PendingServicesController.cs` -- **操作**: 删除文件 -- **影响**: 移除 `/api/gateway/pending-services/*` API - -### 4. 数据模型 -- **文件**: `src/yarpgateway/Models/GwPendingServiceDiscovery.cs` -- **操作**: 删除文件 -- **影响**: 移除待处理服务发现实体 - -### 5. DbContext -- **文件**: `src/yarpgateway/Data/GatewayDbContext.cs` -- **操作**: 移除 `DbSet` 属性 - -### 6. 迁移文件(可选) -- **文件**: `src/yarpgateway/Migrations/20260222134342_AddPendingServiceDiscovery.cs` -- **操作**: 保留(数据库已有该表)或删除(如果重新创建数据库) - -## 不需要移除 - -| 组件 | 理由 | -|------|------| -| DatabaseClusterConfigProvider | YARP 集群配置仍然需要 | -| PgSqlConfigChangeListener | 配置监听仍然需要 | -| 现有健康检查 | YARP 内置被动健康检查 | - -## 实现顺序 - -1. 移除 PendingServicesController.cs -2. 移除 KubernetesPendingSyncService.cs -3. 移除 GwPendingServiceDiscovery.cs -4. 更新 GatewayDbContext.cs -5. 更新 Program.cs -6. 更新 ROADMAP.md 标记为完成 -7. 提交代码 - -## 风险 - -- **数据丢失**: 如果数据库已有 `PendingServiceDiscoveries` 表,删除代码后数据仍然存在但无法访问 -- **API 变更**: 移除 `/api/gateway/pending-services/*` 端点,需要通知 console 团队 - -## 验证 - -完成后验证: -- `dotnet build` 成功 -- 无 `KubernetesPendingSyncService` 引用 -- 无 `PendingServicesController` 引用 -- 无 `GwPendingServiceDiscovery` 引用 - ---- - -*计划创建: 2026-03-02* diff --git a/.planning/quick/001-upgrade-platform/001-PLAN.md b/.planning/quick/001-upgrade-platform/001-PLAN.md deleted file mode 100644 index 99260d2..0000000 --- a/.planning/quick/001-upgrade-platform/001-PLAN.md +++ /dev/null @@ -1,39 +0,0 @@ -# Plan: 升级 Fengling.Platform 包到最新 - -## 任务描述 -升级 fengling-gateway 项目中的 Fengling.Platform.Infrastructure 包到最新版本,并修复现有编译警告。 - -## 变更分析 - -### fengling-platform 新版本主要变更 -1. **主键类型变更**:从 `long Id` 改为 `string Id`(Guid) -2. **新增 GwCluster 聚合根**:包含内嵌 Destinations 列表 -3. **GwTenantRoute 扩展**:新增 Match (GwRouteMatch)、Transforms 等字段 -4. **移除GwServiceInstance**:作为 GwCluster 的内嵌值对象 GwDestination -5. **新增值对象**:GwRouteMatch、GwTransform、GwLoadBalancingPolicy、GwHealthCheckConfig、GwSessionAffinityConfig - -### 当前编译警告 -1. **CS0108**: GatewayDbContext.Tenants 隐藏继承成员 PlatformDbContext.Tenants -2. **NU1506**: 重复 PackageVersion 定义 -3. **NU1507**: 配置了多个包源 - -## 任务列表 - -### Task 1: 分析并修复 CS0108 警告 -- **文件**: src/yarpgateway/Data/GatewayDbContext.cs -- **操作**: 将 `Tenants` 属性添加 `new` 关键字,或使用不同的名称避免隐藏 -- **验证**: dotnet build 无 CS0108 警告 - -### Task 2: 修复 NU1506 重复包版本警告 -- **文件**: Directory.Packages.props 或 YarpGateway.csproj -- **操作**: 检查并移除重复的 PackageVersion 定义 -- **验证**: dotnet restore 无 NU1506 警告 - -### Task 3: 配置包源映射解决 NU1507 -- **文件**: NuGet.Config 或 Directory.Build.props -- **操作**: 添加包源映射配置 -- **验证**: dotnet restore 无 NU1507 警告 - -## 验证 -- dotnet build 成功,0 错误 -- 无 CS0108、NU1506、NU1507 警告 diff --git a/.planning/quick/001-upgrade-platform/001-SUMMARY.md b/.planning/quick/001-upgrade-platform/001-SUMMARY.md deleted file mode 100644 index 88215eb..0000000 --- a/.planning/quick/001-upgrade-platform/001-SUMMARY.md +++ /dev/null @@ -1,41 +0,0 @@ -# Quick Task 001 Summary: 升级 Fengling.Platform 包并修复编译警告 - -## 任务概述 -升级 fengling-gateway 项目中的 Fengling.Platform.Infrastructure 包引用,并修复编译警告。 - -## 变更内容 - -### 1. 修复 CS0108 警告 -**文件**: `src/yarpgateway/Data/GatewayDbContext.cs` -**问题**: `GatewayDbContext.Tenants` 隐藏了继承的成员 `PlatformDbContext.Tenants` -**修复**: 添加 `new` 关键字明确表示新定义 -```csharp -// 修复前 -public DbSet Tenants => Set(); -// 修复后 -public new DbSet Tenants => Set(); -``` - -### 2. 修复 NU1506 重复 PackageVersion -**文件**: `src/yarpgateway/Directory.Packages.props` -**问题**: 重复定义了 Microsoft.AspNetCore.Authentication.JwtBearer、Microsoft.EntityFrameworkCore.Design、Microsoft.EntityFrameworkCore -**修复**: 移除重复的 PackageVersion 定义 - -### 3. 修复 NU1507 多个包源警告 -**文件**: 根目录创建 `NuGet.Config` -**问题**: 配置了多个包源(gitea、nuget.org),需要包源映射 -**修复**: 添加 packageSourceMapping 配置到 NuGet.Config,并删除子目录中的重复配置 - -## 构建结果 -- ✅ 0 错误 -- ✅ CS0108 警告已修复 -- ✅ NU1506 警告已修复 -- ✅ NU1507 警告已修复 -- ⚠️ MSB3277 警告(EntityFrameworkCore.Relational 版本冲突)- 来自测试项目依赖,不影响构建 - -## 相关文件 -- `src/yarpgateway/Data/GatewayDbContext.cs` - 添加 new 关键字 -- `src/yarpgateway/Directory.Packages.props` - 移除重复包版本 -- `src/yarpgateway/Directory.Build.props` - 添加包源映射 -- `tests/Directory.Build.props` - 添加包源映射 -- `NuGet.Config` - 新建包源映射配置 diff --git a/Dockerfile b/Dockerfile index e365b9c..bddbf7f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,37 @@ -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +FROM 192.168.100.120:8418/fengling/dotnet/aspnet:10.0 AS base +USER root +RUN apk add --no-cache krb5-libs USER $APP_UID WORKDIR /app EXPOSE 8080 EXPOSE 8081 -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +FROM 192.168.100.120:8418/fengling/dotnet/sdk:10.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src -# Copy project files from src -COPY ["src/Directory.Packages.props", "src/"] -COPY ["src/YarpGateway.csproj", "src/"] -COPY ["src/NuGet.Config", "src/"] +# Copy NuGet config first +COPY ["NuGet.Config", "./"] -# Restore dependencies -RUN dotnet restore "src/YarpGateway.csproj" +# Copy all project files for restore +COPY ["src/yarpgateway/*.props", "src/yarpgateway/"] +COPY ["src/yarpgateway/*.csproj", "src/yarpgateway/"] +COPY ["src/Fengling.Gateway.Plugin.Abstractions/*.csproj", "src/Fengling.Gateway.Plugin.Abstractions/"] # Copy all source code COPY . . -WORKDIR "/src" -RUN dotnet build "src/YarpGateway.csproj" -c $BUILD_CONFIGURATION -o /app/build +# Restore dependencies (after copying all code to ensure project references work) +RUN dotnet restore "src/yarpgateway/YarpGateway.csproj" --configfile "NuGet.Config" + +# Build +WORKDIR "/src/src/yarpgateway" +RUN dotnet build "YarpGateway.csproj" -c $BUILD_CONFIGURATION -o /app/build --no-restore FROM build AS publish ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "src/YarpGateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false +WORKDIR "/src/src/yarpgateway" +RUN dotnet publish "YarpGateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false --no-restore FROM base AS final WORKDIR /app diff --git a/NuGet.Config b/NuGet.Config index 66aa067..f325325 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,16 +1,13 @@ + + + + + - - - - - - - - - + diff --git a/k8s/test/deployment.yaml b/k8s/test/deployment.yaml new file mode 100644 index 0000000..9918012 --- /dev/null +++ b/k8s/test/deployment.yaml @@ -0,0 +1,79 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fengling-gateway + namespace: fengling-test + labels: + app: fengling-gateway + version: v2 +spec: + replicas: 1 + selector: + matchLabels: + app: fengling-gateway + template: + metadata: + labels: + app: fengling-gateway + version: v2 + spec: + containers: + - name: gateway + image: 192.168.100.120:8418/fengling/fengling-gateway:test + imagePullPolicy: Always + ports: + - containerPort: 8080 + name: http + protocol: TCP + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Production" + - name: ASPNETCORE_URLS + value: "http://+:8080" + envFrom: + - configMapRef: + name: fengling-gateway-config + - secretRef: + name: fengling-gateway-secret + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: 1000m + memory: 512Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: fengling-gateway + namespace: fengling-test + labels: + app: fengling-gateway +spec: + type: ClusterIP + selector: + app: fengling-gateway + ports: + - port: 80 + targetPort: 8080 + name: http + - port: 8081 + targetPort: 8081 + name: management diff --git a/k8s/test/secret.yaml b/k8s/test/secret.yaml new file mode 100644 index 0000000..25766a1 --- /dev/null +++ b/k8s/test/secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: fengling-gateway-secret + namespace: fengling-test +type: Opaque +stringData: + # 使用与生产环境相同的数据库(或修改为测试数据库) + ConnectionStrings__DefaultConnection: "Host=81.68.223.70;Port=15432;Database=fengling_gateway;Username=movingsam;Password=sl52788542" + ConnectionStrings__ConsoleConnection: "Host=81.68.223.70;Port=15432;Database=fengling_console;Username=movingsam;Password=sl52788542" + Jwt__Authority: "https://apigateway.shtao1.cn" + Redis__ConnectionString: "81.68.223.70:16379,password=sl52788542" diff --git a/src/yarpgateway/Config/DatabaseRouteConfigProvider.cs b/src/yarpgateway/Config/DatabaseRouteConfigProvider.cs index 00e9d45..28d9d21 100644 --- a/src/yarpgateway/Config/DatabaseRouteConfigProvider.cs +++ b/src/yarpgateway/Config/DatabaseRouteConfigProvider.cs @@ -51,7 +51,7 @@ public class DatabaseRouteConfigProvider await using var dbContext = _dbContextFactory.CreateDbContext(); var routes = await dbContext - .GwTenantRoutes.Where(r => r.Status == 1 && !r.IsDeleted) + .GwRoutes.Where(r => r.Status == 1 && !r.IsDeleted) .ToListAsync(); var newRoutes = new ConcurrentDictionary(); @@ -65,7 +65,6 @@ public class DatabaseRouteConfigProvider Match = new RouteMatch { Path = route.Match?.Path ?? string.Empty }, Metadata = new Dictionary { - ["TenantCode"] = route.TenantCode, ["ServiceName"] = route.ServiceName, }, }; diff --git a/src/yarpgateway/Data/GatewayDbContext.cs b/src/yarpgateway/Data/GatewayDbContext.cs index 59229f5..5751125 100644 --- a/src/yarpgateway/Data/GatewayDbContext.cs +++ b/src/yarpgateway/Data/GatewayDbContext.cs @@ -12,7 +12,7 @@ namespace YarpGateway.Data; public class GatewayDbContext : PlatformDbContext { // DbSet 别名,兼容旧代码 - public DbSet TenantRoutes => GwTenantRoutes; + public DbSet Routes => GwRoutes; public DbSet ServiceInstances => GwClusters; // 服务发现相关 @@ -51,7 +51,7 @@ public class GatewayDbContext : PlatformDbContext { var entries = ChangeTracker.Entries() .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) - .Where(e => e.Entity is GwTenantRoute or GwCluster or Tenant); + .Where(e => e.Entity is GwRoute or GwCluster or Tenant); _configChangeDetected = entries.Any(); } diff --git a/src/yarpgateway/Directory.Packages.props b/src/yarpgateway/Directory.Packages.props index 9bb13b0..771634a 100644 --- a/src/yarpgateway/Directory.Packages.props +++ b/src/yarpgateway/Directory.Packages.props @@ -4,7 +4,7 @@ - + diff --git a/src/yarpgateway/Middleware/TenantRoutingMiddleware.cs b/src/yarpgateway/Middleware/TenantRoutingMiddleware.cs index a5f0c38..fe31f49 100644 --- a/src/yarpgateway/Middleware/TenantRoutingMiddleware.cs +++ b/src/yarpgateway/Middleware/TenantRoutingMiddleware.cs @@ -77,7 +77,7 @@ public class TenantRoutingMiddleware } // 4. 从 RouteCache 获取路由信息 - var route = _routeCache.GetRoute(headerTenantId, serviceName); + var route = _routeCache.GetRoute(serviceName); if (route == null) { _logger.LogDebug("Route not found - Tenant: {Tenant}, Service: {Service}", headerTenantId, serviceName); @@ -89,9 +89,8 @@ public class TenantRoutingMiddleware context.Items["DynamicClusterId"] = route.ClusterId; context.Items["TenantId"] = headerTenantId; // 供 Transform 使用 - var routeType = route.IsGlobal ? "global" : "tenant-specific"; - _logger.LogDebug("Tenant routing - Tenant: {Tenant}, Service: {Service}, Cluster: {Cluster}, Type: {Type}", - headerTenantId, serviceName, route.ClusterId, routeType); + _logger.LogDebug("Tenant routing - Tenant: {Tenant}, Service: {Service}, Cluster: {Cluster}", + headerTenantId, serviceName, route.ClusterId); // 6. 继续执行,由 TenantRoutingTransform 处理 Destination 选择 await _next(context); diff --git a/src/yarpgateway/Migrations/20260201120312_InitialCreate.Designer.cs b/src/yarpgateway/Migrations/20260201120312_InitialCreate.Designer.cs deleted file mode 100644 index 3f76f9c..0000000 --- a/src/yarpgateway/Migrations/20260201120312_InitialCreate.Designer.cs +++ /dev/null @@ -1,209 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using YarpGateway.Data; - -#nullable disable - -namespace YarpGateway.Migrations -{ - [DbContext(typeof(GatewayDbContext))] - [Migration("20260201120312_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClusterId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedBy") - .HasColumnType("bigint"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("DestinationId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Health") - .HasColumnType("integer"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedBy") - .HasColumnType("bigint"); - - b.Property("UpdatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Version") - .HasColumnType("integer"); - - b.Property("Weight") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Health"); - - b.HasIndex("ClusterId", "DestinationId") - .IsUnique(); - - b.ToTable("ServiceInstances"); - }); - - modelBuilder.Entity("YarpGateway.Models.GwTenant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedBy") - .HasColumnType("bigint"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantCode") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("TenantName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedBy") - .HasColumnType("bigint"); - - b.Property("UpdatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("TenantCode") - .IsUnique(); - - b.ToTable("Tenants"); - }); - - modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClusterId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedBy") - .HasColumnType("bigint"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("PathPattern") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Priority") - .HasColumnType("integer"); - - b.Property("ServiceName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantCode") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("UpdatedBy") - .HasColumnType("bigint"); - - b.Property("UpdatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ClusterId"); - - b.HasIndex("TenantCode", "ServiceName") - .IsUnique(); - - b.ToTable("TenantRoutes"); - }); - - modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b => - { - b.HasOne("YarpGateway.Models.GwTenant", null) - .WithMany() - .HasForeignKey("TenantCode") - .HasPrincipalKey("TenantCode") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/yarpgateway/Migrations/20260201120312_InitialCreate.cs b/src/yarpgateway/Migrations/20260201120312_InitialCreate.cs deleted file mode 100644 index 870368f..0000000 --- a/src/yarpgateway/Migrations/20260201120312_InitialCreate.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace YarpGateway.Migrations -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "ServiceInstances", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ClusterId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - DestinationId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Address = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Health = table.Column(type: "integer", nullable: false), - Weight = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "integer", nullable: false), - CreatedBy = table.Column(type: "bigint", nullable: true), - CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedBy = table.Column(type: "bigint", nullable: true), - UpdatedTime = table.Column(type: "timestamp with time zone", nullable: true), - IsDeleted = table.Column(type: "boolean", nullable: false), - Version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ServiceInstances", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Tenants", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - TenantCode = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - TenantName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Status = table.Column(type: "integer", nullable: false), - CreatedBy = table.Column(type: "bigint", nullable: true), - CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedBy = table.Column(type: "bigint", nullable: true), - UpdatedTime = table.Column(type: "timestamp with time zone", nullable: true), - IsDeleted = table.Column(type: "boolean", nullable: false), - Version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Tenants", x => x.Id); - table.UniqueConstraint("AK_Tenants_TenantCode", x => x.TenantCode); - }); - - migrationBuilder.CreateTable( - name: "TenantRoutes", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - TenantCode = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - ServiceName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - ClusterId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - PathPattern = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Priority = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "integer", nullable: false), - CreatedBy = table.Column(type: "bigint", nullable: true), - CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedBy = table.Column(type: "bigint", nullable: true), - UpdatedTime = table.Column(type: "timestamp with time zone", nullable: true), - IsDeleted = table.Column(type: "boolean", nullable: false), - Version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TenantRoutes", x => x.Id); - table.ForeignKey( - name: "FK_TenantRoutes_Tenants_TenantCode", - column: x => x.TenantCode, - principalTable: "Tenants", - principalColumn: "TenantCode", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateIndex( - name: "IX_ServiceInstances_ClusterId_DestinationId", - table: "ServiceInstances", - columns: new[] { "ClusterId", "DestinationId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ServiceInstances_Health", - table: "ServiceInstances", - column: "Health"); - - migrationBuilder.CreateIndex( - name: "IX_TenantRoutes_ClusterId", - table: "TenantRoutes", - column: "ClusterId"); - - migrationBuilder.CreateIndex( - name: "IX_TenantRoutes_TenantCode_ServiceName", - table: "TenantRoutes", - columns: new[] { "TenantCode", "ServiceName" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Tenants_TenantCode", - table: "Tenants", - column: "TenantCode", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ServiceInstances"); - - migrationBuilder.DropTable( - name: "TenantRoutes"); - - migrationBuilder.DropTable( - name: "Tenants"); - } - } -} diff --git a/src/yarpgateway/Migrations/20260201133826_AddIsGlobalToTenantRoute.Designer.cs b/src/yarpgateway/Migrations/20260201133826_AddIsGlobalToTenantRoute.Designer.cs deleted file mode 100644 index 1466a7c..0000000 --- a/src/yarpgateway/Migrations/20260201133826_AddIsGlobalToTenantRoute.Designer.cs +++ /dev/null @@ -1,205 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using YarpGateway.Data; - -#nullable disable - -namespace YarpGateway.Migrations -{ - [DbContext(typeof(GatewayDbContext))] - [Migration("20260201133826_AddIsGlobalToTenantRoute")] - partial class AddIsGlobalToTenantRoute - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClusterId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedBy") - .HasColumnType("bigint"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("DestinationId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Health") - .HasColumnType("integer"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedBy") - .HasColumnType("bigint"); - - b.Property("UpdatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Version") - .HasColumnType("integer"); - - b.Property("Weight") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Health"); - - b.HasIndex("ClusterId", "DestinationId") - .IsUnique(); - - b.ToTable("ServiceInstances"); - }); - - modelBuilder.Entity("YarpGateway.Models.GwTenant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedBy") - .HasColumnType("bigint"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantCode") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("TenantName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedBy") - .HasColumnType("bigint"); - - b.Property("UpdatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("TenantCode") - .IsUnique(); - - b.ToTable("Tenants"); - }); - - modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClusterId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedBy") - .HasColumnType("bigint"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsGlobal") - .HasColumnType("boolean"); - - b.Property("PathPattern") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Priority") - .HasColumnType("integer"); - - b.Property("ServiceName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantCode") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("UpdatedBy") - .HasColumnType("bigint"); - - b.Property("UpdatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ClusterId"); - - b.HasIndex("ServiceName"); - - b.HasIndex("TenantCode"); - - b.HasIndex("ServiceName", "IsGlobal", "Status"); - - b.ToTable("TenantRoutes"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/yarpgateway/Migrations/20260201133826_AddIsGlobalToTenantRoute.cs b/src/yarpgateway/Migrations/20260201133826_AddIsGlobalToTenantRoute.cs deleted file mode 100644 index b2959df..0000000 --- a/src/yarpgateway/Migrations/20260201133826_AddIsGlobalToTenantRoute.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace YarpGateway.Migrations -{ - /// - public partial class AddIsGlobalToTenantRoute : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_TenantRoutes_Tenants_TenantCode", - table: "TenantRoutes"); - - migrationBuilder.DropUniqueConstraint( - name: "AK_Tenants_TenantCode", - table: "Tenants"); - - migrationBuilder.DropIndex( - name: "IX_TenantRoutes_TenantCode_ServiceName", - table: "TenantRoutes"); - - migrationBuilder.AddColumn( - name: "IsGlobal", - table: "TenantRoutes", - type: "boolean", - nullable: false, - defaultValue: false); - - migrationBuilder.CreateIndex( - name: "IX_TenantRoutes_ServiceName", - table: "TenantRoutes", - column: "ServiceName"); - - migrationBuilder.CreateIndex( - name: "IX_TenantRoutes_ServiceName_IsGlobal_Status", - table: "TenantRoutes", - columns: new[] { "ServiceName", "IsGlobal", "Status" }); - - migrationBuilder.CreateIndex( - name: "IX_TenantRoutes_TenantCode", - table: "TenantRoutes", - column: "TenantCode"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_TenantRoutes_ServiceName", - table: "TenantRoutes"); - - migrationBuilder.DropIndex( - name: "IX_TenantRoutes_ServiceName_IsGlobal_Status", - table: "TenantRoutes"); - - migrationBuilder.DropIndex( - name: "IX_TenantRoutes_TenantCode", - table: "TenantRoutes"); - - migrationBuilder.DropColumn( - name: "IsGlobal", - table: "TenantRoutes"); - - migrationBuilder.AddUniqueConstraint( - name: "AK_Tenants_TenantCode", - table: "Tenants", - column: "TenantCode"); - - migrationBuilder.CreateIndex( - name: "IX_TenantRoutes_TenantCode_ServiceName", - table: "TenantRoutes", - columns: new[] { "TenantCode", "ServiceName" }, - unique: true); - - migrationBuilder.AddForeignKey( - name: "FK_TenantRoutes_Tenants_TenantCode", - table: "TenantRoutes", - column: "TenantCode", - principalTable: "Tenants", - principalColumn: "TenantCode", - onDelete: ReferentialAction.Restrict); - } - } -} diff --git a/src/yarpgateway/Migrations/20260222134342_AddPendingServiceDiscovery.Designer.cs b/src/yarpgateway/Migrations/20260222134342_AddPendingServiceDiscovery.Designer.cs deleted file mode 100644 index da40030..0000000 --- a/src/yarpgateway/Migrations/20260222134342_AddPendingServiceDiscovery.Designer.cs +++ /dev/null @@ -1,275 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using YarpGateway.Data; - -#nullable disable - -namespace YarpGateway.Migrations -{ - [DbContext(typeof(GatewayDbContext))] - [Migration("20260222134342_AddPendingServiceDiscovery")] - partial class AddPendingServiceDiscovery - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("YarpGateway.Models.GwPendingServiceDiscovery", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AssignedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("AssignedBy") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("AssignedClusterId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("DiscoveredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DiscoveredPorts") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("K8sClusterIP") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("K8sNamespace") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("K8sServiceName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Labels") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("PodCount") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("DiscoveredAt"); - - b.HasIndex("Status"); - - b.HasIndex("K8sServiceName", "K8sNamespace", "IsDeleted") - .IsUnique(); - - b.ToTable("PendingServiceDiscoveries"); - }); - - modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClusterId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedBy") - .HasColumnType("bigint"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("DestinationId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Health") - .HasColumnType("integer"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedBy") - .HasColumnType("bigint"); - - b.Property("UpdatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Version") - .HasColumnType("integer"); - - b.Property("Weight") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Health"); - - b.HasIndex("ClusterId", "DestinationId") - .IsUnique(); - - b.ToTable("ServiceInstances"); - }); - - modelBuilder.Entity("YarpGateway.Models.GwTenant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedBy") - .HasColumnType("bigint"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantCode") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("TenantName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedBy") - .HasColumnType("bigint"); - - b.Property("UpdatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("TenantCode") - .IsUnique(); - - b.ToTable("Tenants"); - }); - - modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClusterId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedBy") - .HasColumnType("bigint"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsGlobal") - .HasColumnType("boolean"); - - b.Property("PathPattern") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Priority") - .HasColumnType("integer"); - - b.Property("ServiceName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantCode") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("UpdatedBy") - .HasColumnType("bigint"); - - b.Property("UpdatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ClusterId"); - - b.HasIndex("ServiceName"); - - b.HasIndex("TenantCode"); - - b.HasIndex("ServiceName", "IsGlobal", "Status"); - - b.ToTable("TenantRoutes"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/yarpgateway/Migrations/20260222134342_AddPendingServiceDiscovery.cs b/src/yarpgateway/Migrations/20260222134342_AddPendingServiceDiscovery.cs deleted file mode 100644 index d891dea..0000000 --- a/src/yarpgateway/Migrations/20260222134342_AddPendingServiceDiscovery.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace YarpGateway.Migrations -{ - /// - public partial class AddPendingServiceDiscovery : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "PendingServiceDiscoveries", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - K8sServiceName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - K8sNamespace = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - K8sClusterIP = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), - DiscoveredPorts = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - Labels = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), - PodCount = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "integer", nullable: false), - AssignedClusterId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - AssignedBy = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - AssignedAt = table.Column(type: "timestamp with time zone", nullable: true), - DiscoveredAt = table.Column(type: "timestamp with time zone", nullable: false), - IsDeleted = table.Column(type: "boolean", nullable: false), - Version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PendingServiceDiscoveries", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_PendingServiceDiscoveries_DiscoveredAt", - table: "PendingServiceDiscoveries", - column: "DiscoveredAt"); - - migrationBuilder.CreateIndex( - name: "IX_PendingServiceDiscoveries_K8sServiceName_K8sNamespace_IsDel~", - table: "PendingServiceDiscoveries", - columns: new[] { "K8sServiceName", "K8sNamespace", "IsDeleted" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_PendingServiceDiscoveries_Status", - table: "PendingServiceDiscoveries", - column: "Status"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "PendingServiceDiscoveries"); - } - } -} diff --git a/src/yarpgateway/Migrations/GatewayDbContextModelSnapshot.cs b/src/yarpgateway/Migrations/GatewayDbContextModelSnapshot.cs deleted file mode 100644 index 623cbaa..0000000 --- a/src/yarpgateway/Migrations/GatewayDbContextModelSnapshot.cs +++ /dev/null @@ -1,272 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using YarpGateway.Data; - -#nullable disable - -namespace YarpGateway.Migrations -{ - [DbContext(typeof(GatewayDbContext))] - partial class GatewayDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("YarpGateway.Models.GwPendingServiceDiscovery", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AssignedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("AssignedBy") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("AssignedClusterId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("DiscoveredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DiscoveredPorts") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("K8sClusterIP") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("K8sNamespace") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("K8sServiceName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Labels") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("PodCount") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("DiscoveredAt"); - - b.HasIndex("Status"); - - b.HasIndex("K8sServiceName", "K8sNamespace", "IsDeleted") - .IsUnique(); - - b.ToTable("PendingServiceDiscoveries"); - }); - - modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClusterId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedBy") - .HasColumnType("bigint"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("DestinationId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Health") - .HasColumnType("integer"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedBy") - .HasColumnType("bigint"); - - b.Property("UpdatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Version") - .HasColumnType("integer"); - - b.Property("Weight") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Health"); - - b.HasIndex("ClusterId", "DestinationId") - .IsUnique(); - - b.ToTable("ServiceInstances"); - }); - - modelBuilder.Entity("YarpGateway.Models.GwTenant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedBy") - .HasColumnType("bigint"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantCode") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("TenantName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedBy") - .HasColumnType("bigint"); - - b.Property("UpdatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("TenantCode") - .IsUnique(); - - b.ToTable("Tenants"); - }); - - modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClusterId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedBy") - .HasColumnType("bigint"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsGlobal") - .HasColumnType("boolean"); - - b.Property("PathPattern") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Priority") - .HasColumnType("integer"); - - b.Property("ServiceName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantCode") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("UpdatedBy") - .HasColumnType("bigint"); - - b.Property("UpdatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ClusterId"); - - b.HasIndex("ServiceName"); - - b.HasIndex("TenantCode"); - - b.HasIndex("ServiceName", "IsGlobal", "Status"); - - b.ToTable("TenantRoutes"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/yarpgateway/Migrations/pending_service_migration.sql b/src/yarpgateway/Migrations/pending_service_migration.sql deleted file mode 100644 index b5097ad..0000000 --- a/src/yarpgateway/Migrations/pending_service_migration.sql +++ /dev/null @@ -1,122 +0,0 @@ -CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( - "MigrationId" character varying(150) NOT NULL, - "ProductVersion" character varying(32) NOT NULL, - CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") -); - -START TRANSACTION; -CREATE TABLE "ServiceInstances" ( - "Id" bigint GENERATED BY DEFAULT AS IDENTITY, - "ClusterId" character varying(100) NOT NULL, - "DestinationId" character varying(100) NOT NULL, - "Address" character varying(200) NOT NULL, - "Health" integer NOT NULL, - "Weight" integer NOT NULL, - "Status" integer NOT NULL, - "CreatedBy" bigint, - "CreatedTime" timestamp with time zone NOT NULL, - "UpdatedBy" bigint, - "UpdatedTime" timestamp with time zone, - "IsDeleted" boolean NOT NULL, - "Version" integer NOT NULL, - CONSTRAINT "PK_ServiceInstances" PRIMARY KEY ("Id") -); - -CREATE TABLE "Tenants" ( - "Id" bigint GENERATED BY DEFAULT AS IDENTITY, - "TenantCode" character varying(50) NOT NULL, - "TenantName" character varying(100) NOT NULL, - "Status" integer NOT NULL, - "CreatedBy" bigint, - "CreatedTime" timestamp with time zone NOT NULL, - "UpdatedBy" bigint, - "UpdatedTime" timestamp with time zone, - "IsDeleted" boolean NOT NULL, - "Version" integer NOT NULL, - CONSTRAINT "PK_Tenants" PRIMARY KEY ("Id"), - CONSTRAINT "AK_Tenants_TenantCode" UNIQUE ("TenantCode") -); - -CREATE TABLE "TenantRoutes" ( - "Id" bigint GENERATED BY DEFAULT AS IDENTITY, - "TenantCode" character varying(50) NOT NULL, - "ServiceName" character varying(100) NOT NULL, - "ClusterId" character varying(100) NOT NULL, - "PathPattern" character varying(200) NOT NULL, - "Priority" integer NOT NULL, - "Status" integer NOT NULL, - "CreatedBy" bigint, - "CreatedTime" timestamp with time zone NOT NULL, - "UpdatedBy" bigint, - "UpdatedTime" timestamp with time zone, - "IsDeleted" boolean NOT NULL, - "Version" integer NOT NULL, - CONSTRAINT "PK_TenantRoutes" PRIMARY KEY ("Id"), - CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode" FOREIGN KEY ("TenantCode") REFERENCES "Tenants" ("TenantCode") ON DELETE RESTRICT -); - -CREATE UNIQUE INDEX "IX_ServiceInstances_ClusterId_DestinationId" ON "ServiceInstances" ("ClusterId", "DestinationId"); - -CREATE INDEX "IX_ServiceInstances_Health" ON "ServiceInstances" ("Health"); - -CREATE INDEX "IX_TenantRoutes_ClusterId" ON "TenantRoutes" ("ClusterId"); - -CREATE UNIQUE INDEX "IX_TenantRoutes_TenantCode_ServiceName" ON "TenantRoutes" ("TenantCode", "ServiceName"); - -CREATE UNIQUE INDEX "IX_Tenants_TenantCode" ON "Tenants" ("TenantCode"); - -INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20260201120312_InitialCreate', '10.0.2'); - -COMMIT; - -START TRANSACTION; -ALTER TABLE "TenantRoutes" DROP CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode"; - -ALTER TABLE "Tenants" DROP CONSTRAINT "AK_Tenants_TenantCode"; - -DROP INDEX "IX_TenantRoutes_TenantCode_ServiceName"; - -ALTER TABLE "TenantRoutes" ADD "IsGlobal" boolean NOT NULL DEFAULT FALSE; - -CREATE INDEX "IX_TenantRoutes_ServiceName" ON "TenantRoutes" ("ServiceName"); - -CREATE INDEX "IX_TenantRoutes_ServiceName_IsGlobal_Status" ON "TenantRoutes" ("ServiceName", "IsGlobal", "Status"); - -CREATE INDEX "IX_TenantRoutes_TenantCode" ON "TenantRoutes" ("TenantCode"); - -INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20260201133826_AddIsGlobalToTenantRoute', '10.0.2'); - -COMMIT; - -START TRANSACTION; -CREATE TABLE "PendingServiceDiscoveries" ( - "Id" bigint GENERATED BY DEFAULT AS IDENTITY, - "K8sServiceName" character varying(255) NOT NULL, - "K8sNamespace" character varying(255) NOT NULL, - "K8sClusterIP" character varying(50), - "DiscoveredPorts" character varying(500) NOT NULL, - "Labels" character varying(2000) NOT NULL, - "PodCount" integer NOT NULL, - "Status" integer NOT NULL, - "AssignedClusterId" character varying(100), - "AssignedBy" character varying(100), - "AssignedAt" timestamp with time zone, - "DiscoveredAt" timestamp with time zone NOT NULL, - "IsDeleted" boolean NOT NULL, - "Version" integer NOT NULL, - CONSTRAINT "PK_PendingServiceDiscoveries" PRIMARY KEY ("Id") -); - -CREATE INDEX "IX_PendingServiceDiscoveries_DiscoveredAt" ON "PendingServiceDiscoveries" ("DiscoveredAt"); - -CREATE UNIQUE INDEX "IX_PendingServiceDiscoveries_K8sServiceName_K8sNamespace_IsDel~" ON "PendingServiceDiscoveries" ("K8sServiceName", "K8sNamespace", "IsDeleted"); - -CREATE INDEX "IX_PendingServiceDiscoveries_Status" ON "PendingServiceDiscoveries" ("Status"); - -INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20260222134342_AddPendingServiceDiscovery', '10.0.2'); - -COMMIT; - diff --git a/src/yarpgateway/Migrations/script.sql b/src/yarpgateway/Migrations/script.sql deleted file mode 100644 index 9a8b36d..0000000 --- a/src/yarpgateway/Migrations/script.sql +++ /dev/null @@ -1,89 +0,0 @@ -CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( - "MigrationId" character varying(150) NOT NULL, - "ProductVersion" character varying(32) NOT NULL, - CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") -); - -START TRANSACTION; -CREATE TABLE "ServiceInstances" ( - "Id" bigint GENERATED BY DEFAULT AS IDENTITY, - "ClusterId" character varying(100) NOT NULL, - "DestinationId" character varying(100) NOT NULL, - "Address" character varying(200) NOT NULL, - "Health" integer NOT NULL, - "Weight" integer NOT NULL, - "Status" integer NOT NULL, - "CreatedBy" bigint, - "CreatedTime" timestamp with time zone NOT NULL, - "UpdatedBy" bigint, - "UpdatedTime" timestamp with time zone, - "IsDeleted" boolean NOT NULL, - "Version" integer NOT NULL, - CONSTRAINT "PK_ServiceInstances" PRIMARY KEY ("Id") -); - -CREATE TABLE "Tenants" ( - "Id" bigint GENERATED BY DEFAULT AS IDENTITY, - "TenantCode" character varying(50) NOT NULL, - "TenantName" character varying(100) NOT NULL, - "Status" integer NOT NULL, - "CreatedBy" bigint, - "CreatedTime" timestamp with time zone NOT NULL, - "UpdatedBy" bigint, - "UpdatedTime" timestamp with time zone, - "IsDeleted" boolean NOT NULL, - "Version" integer NOT NULL, - CONSTRAINT "PK_Tenants" PRIMARY KEY ("Id"), - CONSTRAINT "AK_Tenants_TenantCode" UNIQUE ("TenantCode") -); - -CREATE TABLE "TenantRoutes" ( - "Id" bigint GENERATED BY DEFAULT AS IDENTITY, - "TenantCode" character varying(50) NOT NULL, - "ServiceName" character varying(100) NOT NULL, - "ClusterId" character varying(100) NOT NULL, - "PathPattern" character varying(200) NOT NULL, - "Priority" integer NOT NULL, - "Status" integer NOT NULL, - "CreatedBy" bigint, - "CreatedTime" timestamp with time zone NOT NULL, - "UpdatedBy" bigint, - "UpdatedTime" timestamp with time zone, - "IsDeleted" boolean NOT NULL, - "Version" integer NOT NULL, - CONSTRAINT "PK_TenantRoutes" PRIMARY KEY ("Id"), - CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode" FOREIGN KEY ("TenantCode") REFERENCES "Tenants" ("TenantCode") ON DELETE RESTRICT -); - -CREATE UNIQUE INDEX "IX_ServiceInstances_ClusterId_DestinationId" ON "ServiceInstances" ("ClusterId", "DestinationId"); - -CREATE INDEX "IX_ServiceInstances_Health" ON "ServiceInstances" ("Health"); - -CREATE INDEX "IX_TenantRoutes_ClusterId" ON "TenantRoutes" ("ClusterId"); - -CREATE UNIQUE INDEX "IX_TenantRoutes_TenantCode_ServiceName" ON "TenantRoutes" ("TenantCode", "ServiceName"); - -CREATE UNIQUE INDEX "IX_Tenants_TenantCode" ON "Tenants" ("TenantCode"); - -INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20260201120312_InitialCreate', '9.0.0'); - -ALTER TABLE "TenantRoutes" DROP CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode"; - -ALTER TABLE "Tenants" DROP CONSTRAINT "AK_Tenants_TenantCode"; - -DROP INDEX "IX_TenantRoutes_TenantCode_ServiceName"; - -ALTER TABLE "TenantRoutes" ADD "IsGlobal" boolean NOT NULL DEFAULT FALSE; - -CREATE INDEX "IX_TenantRoutes_ServiceName" ON "TenantRoutes" ("ServiceName"); - -CREATE INDEX "IX_TenantRoutes_ServiceName_IsGlobal_Status" ON "TenantRoutes" ("ServiceName", "IsGlobal", "Status"); - -CREATE INDEX "IX_TenantRoutes_TenantCode" ON "TenantRoutes" ("TenantCode"); - -INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20260201133826_AddIsGlobalToTenantRoute', '9.0.0'); - -COMMIT; - diff --git a/src/yarpgateway/Program.cs b/src/yarpgateway/Program.cs index 566e36e..4338161 100644 --- a/src/yarpgateway/Program.cs +++ b/src/yarpgateway/Program.cs @@ -141,9 +141,8 @@ builder.Services.AddMemoryCache(); // 注册租户路由转换器 builder.Services.AddSingleton(); -// 配置 YARP 反向代理 +// 配置 YARP 反向代理 - 使用数据库配置 builder.Services.AddReverseProxy() - .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) .AddTransforms(transformBuilder => { transformBuilder.AddRequestTransform(async context => @@ -172,7 +171,31 @@ app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = Dat app.MapControllers(); app.MapReverseProxy(); -await app.Services.GetRequiredService().InitializeAsync(); +// Gateway: 确保数据库表存在(在测试环境中自动创建表) +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + try + { + Log.Information("Ensuring Gateway database tables exist..."); + await dbContext.Database.EnsureCreatedAsync(); + Log.Information("Gateway database tables ready"); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to ensure database tables exist"); + } +} + +// 初始化路由缓存 +try +{ + await app.Services.GetRequiredService().InitializeAsync(); +} +catch (Exception ex) +{ + Log.Error(ex, "Failed to initialize route cache."); +} try { diff --git a/src/yarpgateway/Services/PgSqlConfigChangeListener.cs b/src/yarpgateway/Services/PgSqlConfigChangeListener.cs index 241fd06..db2d819 100644 --- a/src/yarpgateway/Services/PgSqlConfigChangeListener.cs +++ b/src/yarpgateway/Services/PgSqlConfigChangeListener.cs @@ -142,7 +142,7 @@ public class PgSqlConfigChangeListener : BackgroundService await using var scope = _serviceProvider.CreateAsyncScope(); await using var db = scope.ServiceProvider.GetRequiredService(); - var currentRouteVersion = await db.GwTenantRoutes + var currentRouteVersion = await db.GwRoutes .OrderByDescending(r => r.Version) .Select(r => r.Version) .FirstOrDefaultAsync(stoppingToken); @@ -176,7 +176,7 @@ public class PgSqlConfigChangeListener : BackgroundService await using var scope = _serviceProvider.CreateAsyncScope(); await using var db = scope.ServiceProvider.GetRequiredService(); - _lastRouteVersion = await db.GwTenantRoutes + _lastRouteVersion = await db.GwRoutes .OrderByDescending(r => r.Version) .Select(r => r.Version) .FirstOrDefaultAsync(stoppingToken); diff --git a/src/yarpgateway/Services/RouteCache.cs b/src/yarpgateway/Services/RouteCache.cs index bb2cb44..dcb4c13 100644 --- a/src/yarpgateway/Services/RouteCache.cs +++ b/src/yarpgateway/Services/RouteCache.cs @@ -12,14 +12,13 @@ public class RouteInfo public string ClusterId { get; set; } = string.Empty; public string PathPattern { get; set; } = string.Empty; public int Priority { get; set; } - public bool IsGlobal { get; set; } } public interface IRouteCache { Task InitializeAsync(); Task ReloadAsync(); - RouteInfo? GetRoute(string tenantCode, string serviceName); + RouteInfo? GetRoute(string serviceName); RouteInfo? GetRouteByPath(string path); } @@ -28,8 +27,7 @@ public class RouteCache : IRouteCache private readonly IDbContextFactory _dbContextFactory; private readonly ILogger _logger; - private readonly ConcurrentDictionary _globalRoutes = new(); - private readonly ConcurrentDictionary> _tenantRoutes = new(); + private readonly ConcurrentDictionary _routes = new(); private readonly ConcurrentDictionary _pathRoutes = new(); private readonly ReaderWriterLockSlim _lock = new(); @@ -42,57 +40,47 @@ public class RouteCache : IRouteCache public async Task InitializeAsync() { _logger.LogInformation("Initializing route cache from database..."); - await LoadFromDatabaseAsync(); - _logger.LogInformation("Route cache initialized: {GlobalCount} global routes, {TenantCount} tenant routes", - _globalRoutes.Count, _tenantRoutes.Count); + try + { + await LoadFromDatabaseAsync(); + _logger.LogInformation("Route cache initialized: {Count} routes", _routes.Count); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to load routes from database. This is normal if database is not initialized yet."); + } } public async Task ReloadAsync() { _logger.LogInformation("Reloading route cache..."); await LoadFromDatabaseAsync(); - _logger.LogInformation("Route cache reloaded"); + _logger.LogInformation("Route cache reloaded: {Count} routes", _routes.Count); } - public RouteInfo? GetRoute(string? tenantCode, string? serviceName) + public RouteInfo? GetRoute(string? serviceName) { - // 参数校验 if (string.IsNullOrEmpty(serviceName)) { _logger.LogDebug("GetRoute called with null or empty serviceName"); return null; } - tenantCode ??= string.Empty; - - _lock.EnterUpgradeableReadLock(); + _lock.EnterReadLock(); try { - // 1. 优先查找租户专用路由 - if (!string.IsNullOrEmpty(tenantCode) && - _tenantRoutes.TryGetValue(tenantCode, out var tenantRouteMap) && - tenantRouteMap.TryGetValue(serviceName, out var tenantRoute)) + if (_routes.TryGetValue(serviceName, out var route)) { - _logger.LogDebug("Found tenant-specific route: {Tenant}/{Service} -> {Cluster}", - tenantCode, serviceName, tenantRoute.ClusterId); - return tenantRoute; + _logger.LogDebug("Found route: {Service} -> {Cluster}", serviceName, route.ClusterId); + return route; } - // 2. 查找全局路由 - if (_globalRoutes.TryGetValue(serviceName, out var globalRoute)) - { - _logger.LogDebug("Found global route: {Service} -> {Cluster} for tenant {Tenant}", - serviceName, globalRoute.ClusterId, tenantCode); - return globalRoute; - } - - // 3. 没找到 - _logger.LogWarning("No route found for: {Tenant}/{Service}", tenantCode, serviceName); + _logger.LogWarning("No route found for: {Service}", serviceName); return null; } finally { - _lock.ExitUpgradeableReadLock(); + _lock.ExitReadLock(); } } @@ -105,15 +93,23 @@ public class RouteCache : IRouteCache { using var db = _dbContextFactory.CreateDbContext(); - var routes = await db.GwTenantRoutes - .Where(r => r.Status == 1 && !r.IsDeleted) - .ToListAsync(); + List routes; + try + { + routes = await db.GwRoutes + .Where(r => r.Status == 1 && !r.IsDeleted) + .ToListAsync(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Database table not found. Returning empty route list."); + routes = new List(); + } _lock.EnterWriteLock(); try { - _globalRoutes.Clear(); - _tenantRoutes.Clear(); + _routes.Clear(); _pathRoutes.Clear(); foreach (var route in routes) @@ -125,34 +121,14 @@ public class RouteCache : IRouteCache Id = route.Id, ClusterId = route.ClusterId, PathPattern = pathPattern, - Priority = route.Priority, - IsGlobal = route.IsGlobal + Priority = route.Priority }; - // 1. 全局路由 - if (route.IsGlobal) - { - _globalRoutes[route.ServiceName] = routeInfo; - _pathRoutes[pathPattern] = routeInfo; - _logger.LogDebug("Loaded global route: {Service} -> {Cluster}", - route.ServiceName, route.ClusterId); - } + _routes[route.ServiceName] = routeInfo; + _pathRoutes[pathPattern] = routeInfo; - // 2. 租户专属路由(无论 IsGlobal 值如何,只要有 TenantCode 就添加到租户路由表) - if (!string.IsNullOrEmpty(route.TenantCode)) - { - _tenantRoutes.GetOrAdd(route.TenantCode, _ => new ConcurrentDictionary()) - [route.ServiceName] = routeInfo; - - // 如果不是全局路由,也添加到路径路由 - if (!route.IsGlobal) - { - _pathRoutes[pathPattern] = routeInfo; - } - - _logger.LogDebug("Loaded tenant route: {Tenant}/{Service} -> {Cluster}", - route.TenantCode, route.ServiceName, route.ClusterId); - } + _logger.LogDebug("Loaded route: {Service} -> {Cluster}", + route.ServiceName, route.ClusterId); } } finally diff --git a/src/yarpgateway/appsettings.json b/src/yarpgateway/appsettings.json index c06a863..0d061ce 100644 --- a/src/yarpgateway/appsettings.json +++ b/src/yarpgateway/appsettings.json @@ -19,10 +19,11 @@ "DefaultConnection": "" }, "Jwt": { - "Authority": "", + "Authority": "http://auth.fengling.local:30080", "Audience": "fengling-gateway", "ValidateIssuer": true, - "ValidateAudience": true + "ValidateAudience": true, + "RequireHttpsMetadata": false }, "Redis": { "ConnectionString": "", @@ -64,4 +65,4 @@ "ServiceDiscovery": { "UseInClusterConfig": true } -} \ No newline at end of file +} diff --git a/网关配置的新想法.md b/网关配置的新想法.md deleted file mode 100644 index e2f1660..0000000 --- a/网关配置的新想法.md +++ /dev/null @@ -1,23 +0,0 @@ -#### 网关配置的新想法 - -路由/集群/目标 等配置还是通过数据库变更通知网关进行重新加载的方式触发变更 - -k8s 的service中需要有固定的label来约定产生新的配置 -以下是范例 -service-label - - app-router-host = https://hostname #代表网关域名地址 - - app-router-name = member # 代表路由名 - - app-router-prefix = /member # 代表路由前缀 - - app-cluster-name = member #代表集群名(id) - - app-cluster-destination = default # 代表标准服务目标 如果不是default 比如 1668 则代表企业编号独有的目标 -详细请求说明: -比如一个请求进来了 请求路径是 {host}/member/api/v1/memberinfo/{id} header:{authorization: bearer xxx} --> 根据 host+ prefix匹配到 member路由 -> 进入到对应的cluster -> -中间件进行解析或者是yarp-transform能做到最好使用yarp-transform来处理 或者如果我集成了openiddict是否可以拿到jwt这部分的信息 --> 解析出 customer/或者是租户id都行 只要能表示租户的都算 -> 找到对应的租户就进对应的目标服务 找不到就进 标准服务目标 - -配置生效详细说明: -1.在console项目中监听k8s 的服务 如果发现有新的app-router-name 相关信息就要产生待执行配置 用户明确确认后 生成数据库配置 在此之前只能存在于内存/缓存中 ; -2.同样的监听服务 发现app-cluster-name 检查是否有新的app-cluster-name 如果有新的 同样产生待执行配置 后续于路由一致 -3.同样监听服务 发现app-cluster-destination 检查对应的cluster是否存在这个destination 如果不存在 同样产生待执行配置 后续与上述一致 -4.用户确认是否执行这些配置 用户加载到界面后可调整配置,同意后产生持久化数据到数据库,点击立即生效才下发到yarp网关 网关重新根据新的配置加载到最新配置后生效 \ No newline at end of file