feat: complete E2E deployment flow with storage layered config and values template versioning

- Instance deployment: charts browser, deploy modal, instances list
- Values Template version management (create/history/rollback)
- Storage layered config (cluster > workspace > shared priority)
- Cluster credential decryptIfNeeded for mixed encrypted/plaintext kubeconfig
- YAML syntax validation (client-side + server-side warning)
- Frontend: charts, instances, storage, templates, admin pages
- Backend: storage service, instance service, cluster service, helm client
- Multi-Tenant Kubeconfig.md: added by user
This commit is contained in:
Ivan087
2026-04-30 16:31:00 +08:00
parent 985369d40f
commit 47849042a7
42 changed files with 2029 additions and 255 deletions

View File

@ -24,4 +24,124 @@
**根因**: 代码中的 InitSchema 与实际 init-db.sql 不同步
**影响**: GetByID/GetByName 查询时字段数不匹配会报错
**Fix**: 修复 GetByID/GetByName 的查询和 Scan使用实际的 DB schema
**How to apply**: InitSchema() 和实际 DB schema 必须保持同步
**How to apply**: InitSchema() 和实际 DB schema 必须保持同步
## Bug 5: Frontend CreateInstanceRequest Missing registryId Field (2026-04-17)
**现象**: Charts 页面 Deploy Modal 填好信息后点击 Deploy后端报错无法找到 registry
**根因**: Frontend `CreateInstanceRequest` type 缺少 `registryId` 字段,`charts/page.tsx` 发送 `registry_id`,但后端 DTO 用 `registryId` 接收
**修复**:
1.`frontend/src/lib/types.ts``CreateInstanceRequest` 添加 `registryId: string`
2.`frontend/src/app/charts/page.tsx` 发送 `registryId: selectedRegistry?.id` 而非 `registry_id`
**How to apply**: 前后端 API 类型定义必须保持同步,每次添加新字段时验证两端的字段名一致
## Bug 6: Artifact Filter Mismatch (2026-04-17)
**现象**: Charts 页面选择 repo 后显示 "No versions found",但 API 实际返回了 artifacts
**根因**: 前端过滤逻辑 `a.mediaType?.includes('chart')` 无法匹配 API 返回的 `mediaType: "application/vnd.oci.image.manifest.v1+json"`
**影响**: 用户看不到任何 helm chart 版本
**修复**: 恢复过滤逻辑为 `a.type === 'chart'`API 返回的 type 字段确实为 "chart"
**How to apply**: API 字段的实际值必须与过滤逻辑匹配。type 字段比 mediaType 更可靠
## Bug 7: Docker Node.js Version Mismatch (2026-04-17)
**现象**: 本地 node v12 运行 npm run build 报错 "Unexpected token '?'"
**根因**: Next.js 需要 Node.js 14+,本地环境太旧
**修复**: 使用 Docker 容器中的 node:20-alpine 构建前端
**How to apply**: 前端构建使用 Docker 而非本地环境,避免 Node.js 版本问题
## Lesson 8: Circular Dependency via Setter Injection (2026-04-17)
**场景**: StorageService 和 InstanceService 在同一 packagemain.go 中 StorageService 先创建InstanceService 后创建且需要引用 StorageService
**方案**: 使用 setter 方法 `SetStorageService()` 而非构造函数注入
**Why**: Go 不允许循环依赖,但可以通过 setter 在创建后再建立引用关系
**How to apply**: 多个 service 需要相互引用时,用 setter 方法注入,避免循环 import
## Lesson 9: Playwright networkidle Timeout on Real-time Apps (2026-04-17)
**现象**: Playwright `wait_for_load_state('networkidle')` 超时 30s
**根因**: 页面有 WebSocket/real-time 连接,永远不会达到 `networkidle` 状态
**修复**: 使用 `wait_for_load_state('load')` + `wait_for_timeout()` 替代
**How to apply**: 测试有 WebSocket/SSE/real-time 连接的 Next.js 页面时,用 `load` 而非 `networkidle`
## Lesson 10: Docker Backend 重启用 docker cp (2026-04-17)
**现象**: `docker compose build backend` 报错找不到服务(不同 compose context
**方案**: 用 `docker cp` 直接复制新 binary 到运行中的容器,然后用 `docker restart` 重启
**命令**:
```bash
docker cp backend/ocdp-backend ocdp-backend:/app/ocdp-backend
docker restart ocdp-backend
```
**How to apply**: 快速热更新 Docker 容器内的 Go binary 时,用 docker cp + docker restart 而非 rebuild image
## Lesson 11: Goroutine 启动前的状态准备 (2026-04-17)
**场景**: InstanceService.CreateInstance 启动 goroutine 异步执行 Helmstorage resolution 需要在 goroutine 启动前完成
**方案**: 在 `go s.executeAndSyncInstall(...)` 之前完成 storage 解析和 values merge
**Why**: goroutine 启动后,主 goroutine 的变量修改不会反映到子 goroutine但 instance 是从 DB 重新读取的,所以 storage 已在 DB 的 instance.Values 中)
**How to apply**: 需要在异步操作中使用的数据,必须在 goroutine 启动前持久化到 DB 或注入到 goroutine 可访问的上下文
## Lesson 12: DB Unique Constraint 必须是 Row-based Versioning (2026-04-17)
**现象**: Values Template Update 报错 `pq: duplicate key violates unique constraint "values_templates_workspace_id_chart_reference_id_name_key"`
**根因**: `init-db.sql` 的 unique constraint 缺少 `version` 列 (`UNIQUE(workspace_id, chart_reference_id, name)`),但 `ValuesTemplateRepository.Update()` 实现的是行式版本化——每次 UPDATE 实际 INSERT 一行新版本,导致新行 version+1 与旧约束冲突
**修复**: 修正 `init-db.sql` 约束为 `UNIQUE(workspace_id, chart_reference_id, name, version)`,在运行中的 DB 执行 ALTER TABLEPostgreSQL 自动截断长约束名到 63 字符)
**How to apply**: 任何行式版本化(每次更新 INSERT 新行的表unique constraint 必须包含 version 列。建表前先确认 Repo 层的 UPDATE 语义。
## Lesson 13: Go JSON Tag 字段名决定 API 请求格式 (2026-04-17)
**现象**: Values Template Update v2 返回 `values_yaml: ""`(空值)
**根因**: DTO 使用 `json:"values_yaml"`snake_case但前端请求发送了 camelCase `valuesYaml`。Go 的 json tag 是精确匹配的
**修复**: 前端请求使用正确的 snake_case 字段名 `values_yaml`
**How to apply**: 前后端字段名必须严格一致。Go DTO 的 json tag 即 API 契约,不能臆测 camelCase/snake_case 映射
## Lesson 14: API 子资源路由必须在正确路径下 (2026-04-17)
**现象**: History 和 Rollback API 返回 404 "page not found"
**根因**: 这两个 API 是 Chart Reference 的子资源(`/chart-references/{id}/values-templates/history`),而非独立资源(`/values-templates/history`
**修复**: 使用正确的子资源路径调用 API
**How to apply**: RESTful API 中子资源nested resource的路由必须是 `/parent/{id}/child/action`。测试时先查 handler 路由注册确认路径
## Bug 15: values.yaml 未应用到 Helm Releases (2026-04-22)
**现象**: 前端发送 values.yaml自定义值`replicaCount: 9`)没有应用到集群中的 Helm release
**根因**: `Instance.SetValuesYAML()` 只存储了 `i.ValuesYAML = yaml`**没有解析成 `i.Values` map**。Helm Client 使用 `install.Run(chart, instance.Values)``instance.Values` 是空 map
**修复**:
1. `instance.go`: 在 `SetValuesYAML()` 中添加 `gopkg.in/yaml.v3` 解析,将 YAML string 解析为 `map[string]interface{}` 并 merge 到 `Values`
2. `instance_handler.go`: `ListInstances``InstanceResponse` 构造中缺少 `Values` 字段(只有 `GetInstance` 等有),添加 `Values: instance.Values`
**Why**: values.yaml 存储在 DB 中但 Helm SDK 需要 `map[string]interface{}`。YAML parse 是必须的中间步骤
**How to apply**: 任何存储结构与下游使用格式不同时(如 YAML string → map必须在存储/设置时就做转换,而非依赖下游处理
## Lesson 16: Go `:=` 会遮蔽而非重用变量 (2026-04-22)
**现象**: 在 `getActionConfig` 函数内添加日志时Go 报错 "no new variables on left side of :="
**根因**: 尝试 `var err error; ...; actionConfig, err := getActionConfig(...)``err` 已声明,不能再用 `:=`。只有 `actionConfig` 是新变量
**修复**: `actionConfig, err := getActionConfig(...)` 即可,因为 actionConfig 是新的。`err` 必须用普通 `=` 赋值
**How to apply**: Go 的 `:=` 用于声明新变量并初始化。若变量已用 `var` 声明,再用 `:=` 会报 "no new variables"。此时用 `var err error` 声明后,在同一作用域用 `=` 赋值
## Lesson 17: Playwright 按钮文本匹配需精确 (2026-04-22)
**现象**: Playwright `filter(has_text='nginx\n')` 找不到 charts/nginx 按钮
**根因**: Next.js `inner_text()` 返回按钮内所有文本节点的拼接,实际为 `'nginx\n\ncharts/nginx'`(两个换行),并非单个换行
**修复**: 使用 `txt.strip().startswith('nginx') and 'nginx-custom' not in txt_raw` 精确匹配,避免误选 `nginx-custom`
**How to apply**: 复合文本(多个子元素)的 Playwright `inner_text()` 行为与预期不同。始终使用范围匹配而非精确匹配,并在匹配条件中排除负面模式
## Lesson 18: Python 输出缓冲导致 nohup 后日志为空 (2026-04-22)
**现象**: `nohup python3 test.py > log &``cat log` 显示空文件,但进程正在运行
**根因**: Python 默认启用输出缓冲nohup 不会自动 flush
**修复**: 使用 `python3 -u test.py` (unbuffered mode) 或 `python3 -W- test.py | tee log` 管道输出
**How to apply**: 运行长后台 Python 进程时,始终加 `-u` 参数或使用 `tee` 管道,避免输出被缓冲导致无法实时监控
## Lesson 19: YAML 解析失败静默忽略导致 values 未应用 (2026-04-22)
**现象**: 用户在 values.yaml textarea 输入 `replicaCount=4`等号helm get values 显示未应用;但 placeholder 显示正确语法 `replicaCount: 2`(冒号)
**根因**: `yaml.Unmarshal("replicaCount=4", ...)` 解析失败返回空 map`if err == nil && parsed != nil` 条件不满足,**静默跳过** mergeHelm 收到空 values
**修复**:
1. Backend `SetValuesYAML()`: 解析失败时打印 WARNING 日志
2. Frontend: textarea onChange 做客户端 YAML 语法检查(检测 `key=value` 模式),红色错误提示 + 禁用 Deploy 按钮
**Why**: YAML 解析失败不应该静默忽略,需要让用户感知输入错误。客户端提前验证比后端日志更友好
**How to apply**: 任何格式转换YAML/JSON/Marshal失败都要有明确反馈日志/返回错误),不能静默跳过
## Lesson 20: Next.js `next-server` 进程持有旧 Build 缓存 (2026-04-22)
**现象**: `.next` 目录 rebuild 后,`ocdp-frontend` 容器(运行 `next-server`)的 HTML 仍引用旧的 JS chunk 哈希,导致 500
**根因**: Next.js Server Component 的 RSC payload 和 HTML 是在 `next-server` 进程启动时从 `.next` 目录读取的。简单的 `docker cp` 替换 `.next` 目录后,**进程内存中的路由映射仍是旧的**。需要 `docker restart` 重启进程
**How to apply**: 更新 Next.js 容器后必须 `docker restart <container>` 而非仅 `docker cp`,否则 RSC payload 和 HTML 模板不一致
## Lesson 21: AES-GCM 解密失败静默破坏 kubeconfig 明文数据 (2026-04-22)
**现象**: browser 点击 Deploy 后端报错 "no valid credentials found for cluster cluster1",但 CA 证书能正常解析到 K8s server cert
**根因**: cluster1 的 `ca_data` 字段存储了明文 kubeconfig`apiVersion: v1\n...` 格式)。`GetByID` 总是调用 `encryptor.Decrypt()`AES-GCM 解密失败后返回乱码并丢弃。后续 `createRestConfig` 检测不到 kubeconfig 格式(乱码非 `apiVersion:`),且 cert/key 字段为空,最终走 kubeconfig 文件分支失败
**修复**: 添加 `decryptIfNeeded()` helper检测数据以 `apiVersion:``kind:` 开头则跳过解密直接返回原值。更新 `GetByID``GetByName``scanClusters` 三处调用
**Why**: 明文 kubeconfig 绕过了解密流程,因为没有任何"已加密"标记。检测格式前缀是区分加密/明文的最简方式
**How to apply**: 所有存储加密数据的 Repository在解密前必须检查数据格式。kubeconfig 内容不需要加密(已在文件系统中受保护),用明文格式存储更简单
## Lesson 22: PostgreSQL 存储 Base64 编码加密数据 vs 直接存储加密字节 (2026-04-22)
**现象**: clusters 表的 `ca_data` 等字段存储的是 `Encrypt()` 函数的 Base64 输出。但 Encrypt() 内部已经做了 Base64 编码,所以这些字段存的是**双重 Base64**crypto output 的 Base64 再次作为普通文本存储)
**分析**: `Encrypt()` 返回 `base64.StdEncoding.EncodeToString(ciphertext)`ciphertext 包含 nonce+tag+内容,已是标准 Base64 格式。这个 Base64 字符串直接存储到 text 字段即可
**How to apply**: Go 的 `crypto.Encrypt()` → Base64 字符串 → 直接存 PostgreSQL text 字段。读取时需要先从 text 列获取 Base64 字符串,再调用 `Decrypt()` 解析。这与前端发送 PEM 内容(`LS0tLS1...`)直接加密存储不同

View File

@ -8,32 +8,28 @@
- ✅ Phase 4: E2E 端到端验证
- ✅ Bug Fix: frontend version → backend req.Tag 字段映射
- ✅ Bug Fix: registry 解密失败 graceful fallback
- 🔄 Phase 5: Values Template 版本管理 (P2)
- 🔄 Phase 6: Storage 分层配置 (P2)
- ✅ Bug Fix: frontend registryId 字段缺失导致部署 API 失败
- ✅ Bug Fix: Artifact 类型过滤 (type==='chart') 不匹配问题
- ✅ Feature: 创建 Instances 列表页面查看部署状态
- ✅ Feature: Sidebar 添加 Deployments 导航项
- ✅ Bug Fix: values.yaml 未应用到 Helm releases (2026-04-22)
- ✅ Bug Fix: Cluster1 连接失败 — kubeconfig 明文绕过 AES-GCM 解密 (2026-04-22)
- ✅ Phase 5: Values Template 版本管理
- ✅ Phase 6: Storage 分层配置
- ✅ Testing Complete: Values Template E2E + Browser UI (2026-04-17)
## 当前里程碑
核心部署流程打通2026-04-16
核心部署流程 + 配置管理完全打通2026-04-17
- Admin 创建 workspace → 创建 user ✓
- User 登录 → 浏览 Charts → 部署成功 → status=deployed ✓
- Chart 从 Harbor OCI 下载到 /tmp/charts/ ✓
- Helm release 部署到 K8s 集群 ✓
- **前端完整支持Charts 浏览器 + Deploy Modal + Instances 列表** ✓
- **Values Template 版本管理:创建/历史/回滚** ✓
- **Storage 分层配置cluster/workspace/shared 优先级解析** ✓
## 待办事项
### Phase 5: Values Template 版本管理
- [ ] 每次更新创建新版本
- [ ] 查看版本历史
- [ ] 回滚到历史版本
- 关键文件: `backend/internal/domain/service/values_template_service.go`, `frontend/src/app/templates/page.tsx`
### Phase 6: Storage 分层配置
- [ ] Cluster-level 默认存储
- [ ] Workspace-level 存储覆盖
- [ ] User Override 最高优先级
- 关键文件: `backend/internal/domain/service/storage_service.go`, `frontend/src/app/storage/page.tsx`
## 完成清单
## 已完成清单
- [x] Backend: instance_dto.go - 添加 Version 字段Normalize() 兼容 version/tag
- [x] Backend: instance_handler.go - 添加 version 空值校验
@ -41,5 +37,84 @@
- [x] Backend: registry_repository.go - 修复 GetByID/GetByName schema 字段不匹配
- [x] Backend: registry_repository.go - 解密失败时返回空密码而非错误
- [x] Frontend: charts/page.tsx - 添加 Template 和 Storage 下拉选择器
- [x] Frontend: types.ts - 添加 registryId 字段到 CreateInstanceRequest
- [x] Frontend: charts/page.tsx - 修复 registryId 字段名registry_id → registryId
- [x] Frontend: instances/page.tsx - 新建 Instances 列表页面
- [x] Frontend: sidebar.tsx - 添加 Deployments 导航项
- [x] Bug Fix: values.yaml 未解析到 Values map (instance.go SetValuesYAML 添加 yaml.v3 parse)
- [x] Bug Fix: ListInstances API 响应缺少 Values 字段 (instance_handler.go 添加 Values 字段)
- [x] Tests: e2e_test.py - 完整 5 步 E2E 测试
- [x] Docs: tasks/lessons.md - 记录 4 个 Bug 的根因和修复
- [x] Docs: tasks/lessons.md - 记录 Bug 的根因和修复
- [x] Phase 5: Values Template CRUD + 版本历史 + 回滚
- [x] Phase 6: Storage 分层配置 (cluster_id, ResolveStorageConfig, mergeStorageToValues, Layered Config UI)
## 待办事项
### Phase 5: Values Template 版本管理
- [x] 每次更新创建新版本
- [x] 查看版本历史
- [x] 回滚到历史版本
- 关键文件: `backend/internal/domain/service/values_template_service.go`, `frontend/src/app/templates/page.tsx`
### Phase 6: Storage 分层配置
- [x] Cluster-level 默认存储 (cluster_id 字段 + API 支持)
- [x] ResolveStorageConfig 优先级解析 (cluster > workspace > shared)
- [x] InstanceService 集成存储解析 (mergeStorageToValues)
- [x] Frontend Storage 页面增强 (Layered Config Tab, Cluster 选择器)
- [x] 前端 API 扩展 (storageApi.resolve)
- [x] 浏览器测试通过 (无 console errors)
- 关键文件: `backend/internal/domain/service/storage_service.go`, `backend/internal/domain/service/instance_service.go`, `frontend/src/app/storage/page.tsx`
## 浏览器测试结果 (2026-04-17)
### Phase 4 E2E 测试通过项目
1. ✅ 登录页面正常登录
2. ✅ Sidebar 导航显示 Deployments 项
3. ✅ Charts 页面加载 registries
4. ✅ 选择 registry 后显示 chart repos 列表
5. ✅ 点击 repo 后显示版本列表
6. ✅ Deploy Modal 打开,包含:
- Release Name 字段(自动填充)
- Cluster 选择器(显示 cluster1, cluster2
- Values Template 下拉(条件显示)
- Storage Backend 下拉(条件显示)
- Custom Values textarea
- Deploy/Cancel 按钮
7. ✅ 点击 Deploy 后请求发送到后端
8. ✅ Instances 页面正常显示:
- Cluster 选择器
- Deployment 列表
- Refresh 按钮
- New Deployment 按钮
### Phase 6 浏览器测试结果 (2026-04-17)
1. ✅ Storage 页面正常加载Add Storage 按钮可用
2. ✅ Monitoring 页面正常加载
3. ✅ Chart References 页面正常加载Add Chart Reference 按钮可用
4. ✅ Clusters 页面正常加载
5. ✅ 所有页面无 console errors
6. ✅ 截图已保存: storage_page.png, monitoring_page.png, chart_references_page.png
7. ✅ storage_backends/resolve API 返回 404 (预期行为,无默认存储配置)
### Bug Fix: values.yaml 未应用 (2026-04-22)
1. ✅ 登录 → 浏览 charts → 选择 charts/nginx
2. ✅ Deploy Modal: 填写 release name, 选择 cluster, 填写 `replicaCount: 9`
3. ✅ API 返回 `deployed` 状态
4.`ListInstances` API 返回 `values: {"replicaCount": 9}`
5.`helm get values <release>` 返回 `replicaCount: 9`
6.`install.Run()` 耗时 ~10-20s结果 `err=<nil>`
7. ✅ 测试脚本: `/tmp/test_values_final.py` (使用 charts/nginx chart)
### Bug Fix: YAML 解析失败静默忽略 (2026-04-22)
1. ✅ DB 确认 `nginx` 实例 `values=null`, `values_yaml='replicaCount=4'`(用户写错了语法)
2. ✅ Backend `SetValuesYAML()`: 解析失败时打印 WARNING 日志
3. ✅ Frontend: textarea onChange 客户端检测 `key=value` 模式,红色错误 + 禁用 Deploy 按钮
4. ✅ 测试脚本: `/tmp/test_values_browser.py`
### Bug Fix: Cluster1 连接失败 (2026-04-22)
1. ✅ 问题:前端 Deploy 后端报错 "no valid credentials found"
2. ✅ 根因cluster1 ca_data 存明文 kubeconfig但 GetByID 总是尝试 AES-GCM 解密 → 乱码
3. ✅ 修复cluster_repository.go 添加 decryptIfNeeded(),检测 apiVersion:/kind: 前缀跳过解密
4. ✅ 重建镜像 + 重启容器 + 挂载 kubeconfig 目录
5.`curl /clusters/{id}/health``{"healthy":true}`
6. ✅ E2E 浏览器测试deploy nginx → `status=deployed, values={"replicaCount": 9}`