- 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
14 KiB
Lessons Learned
Bug 1: Frontend/Bulk Field Name Mismatch (2026-04-16)
现象: 部署 API 返回 400 "invalid version",即使前端传了正确的 version
根因: 前端发送 JSON 字段 version,但 DTO 只有 Tag(json: tag),handler 读 req.Tag 始终为空
修复: 在 CreateInstanceRequest 中添加 Version 字段,并在 Normalize() 中将 Version 复制到 Tag
How to apply: 前后端接口字段名必须一致。DTO 的 json tag 应与前端发送的字段名匹配,或在 Normalize() 中做兼容映射
Bug 2: Registry Decrypt Fails with Key Mismatch (2026-04-16)
现象: GET /registries 列表正常,但 GET /registries/{id} 返回 404 "failed to decrypt password"
根因: 旧数据用不同 ENCRYPTION_KEY 加密,GetByID/GetByName 解密失败直接返回 error
修复: 解密失败时返回空密码而非错误(与 List 方法行为一致)
Why: 列表查询不触发解密(_ = r.encryptor.Decrypt),但单条查询需要解密。密钥不匹配不应阻断核心业务流程
How to apply: 涉及敏感数据解密时,对密钥不匹配的情况做 graceful fallback 而非直接报错
Bug 3: Docker Compose Project Conflict (2026-04-16)
现象: 从 backend/ 目录运行 docker compose 时报错 "container name already in use" 根因: 容器通过不同 compose 项目启动(ocdp-go vs backend),但使用了相同的容器名 修复: 直接用 docker 命令重启旧容器:docker stop/rm 后用 docker run 启动新镜像 How to apply: 当有多个 compose 文件管理同一网络时,docker run 方式更灵活
Bug 4: InitSchema vs Actual DB Schema Mismatch (2026-04-16)
现象: InitSchema() 创建的 registries 表缺少 workspace_id, owner_id, is_shared 字段 根因: 代码中的 InitSchema 与实际 init-db.sql 不同步 影响: GetByID/GetByName 查询时字段数不匹配会报错 Fix: 修复 GetByID/GetByName 的查询和 Scan,使用实际的 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 接收
修复:
- 在
frontend/src/lib/types.ts的CreateInstanceRequest添加registryId: string - 在
frontend/src/app/charts/page.tsx发送registryId: selectedRegistry?.id而非registry_idHow 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 在同一 package,main.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 重启
命令:
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 异步执行 Helm,storage 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 TABLE(PostgreSQL 自动截断长约束名到 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
修复:
instance.go: 在SetValuesYAML()中添加gopkg.in/yaml.v3解析,将 YAML string 解析为map[string]interface{}并 merge 到Valuesinstance_handler.go:ListInstances的InstanceResponse构造中缺少Values字段(只有GetInstance等有),添加Values: instance.ValuesWhy: 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 条件不满足,静默跳过 merge,Helm 收到空 values
修复:
- Backend
SetValuesYAML(): 解析失败时打印 WARNING 日志 - 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...)直接加密存储不同