diff --git a/Dockerfile b/Dockerfile index a4d5045..ad5c652 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ENV IM_WORKBENCH_CONFIG=/app/configs/workbench.test.json ENV IM_WORKBENCH_INTERNAL_SERVICE_NAME=hotgo-admin ENV IM_WORKBENCH_INTERNAL_SECRET=hotgo-workbench-internal ENV IM_WORKBENCH_MANAGER_BASE_URL=https://admin.118im.com -ENV IM_WORKBENCH_MANAGER_SERVICE_NAME=im-server +ENV IM_WORKBENCH_MANAGER_SERVICE_NAME=im-workbench ENV IM_CONFIG_API_SECRET= ENV IM_INTERNAL_API_SECRET= COPY --from=builder /out/im_workbench /app/im_workbench diff --git a/README.md b/README.md index 118b474..5f39edf 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ X-Request-ID: {uuid} ```text IM_WORKBENCH_MANAGER_BASE_URL=https://admin.118im.com -IM_WORKBENCH_MANAGER_SERVICE_NAME=im-server +IM_WORKBENCH_MANAGER_SERVICE_NAME=im-workbench IM_WORKBENCH_MANAGER_SECRET={IM_CONFIG_API_SECRET} ``` diff --git a/configs/workbench.local.json b/configs/workbench.local.json index 7cf3e73..aaa1443 100644 --- a/configs/workbench.local.json +++ b/configs/workbench.local.json @@ -29,7 +29,7 @@ }, "manager": { "base_url": "", - "service_name": "im-server", + "service_name": "im-workbench", "secret": "", "uid_key_group": "im", "uid_key_keys": [ diff --git a/configs/workbench.test.json b/configs/workbench.test.json index 8b1c227..d50ba12 100644 --- a/configs/workbench.test.json +++ b/configs/workbench.test.json @@ -33,7 +33,7 @@ }, "manager": { "base_url": "https://admin.118im.com", - "service_name": "im-server", + "service_name": "im-workbench", "secret": "", "uid_key_group": "im", "uid_key_keys": [ diff --git a/docs/工作台与Manager内部配置运维说明.md b/docs/工作台与Manager内部配置运维说明.md new file mode 100644 index 0000000..db2066b --- /dev/null +++ b/docs/工作台与Manager内部配置运维说明.md @@ -0,0 +1,442 @@ +# 工作台与 Manager 内部配置运维说明 + +更新时间:2026-09-17 + +## 1. 背景 + +`im_workbench` 需要从 `im_manager` 读取系统配置,例如: + +- `im.uid_encryption_key` + +读取链路如下: + +```text +im_workbench + -> GET https://admin.118im.com/internal/im-config/third-party/im?keys=uid_encryption_key,UID_ENCRYPTION_KEY,uid_key + -> im_manager 校验 HMAC 签名 + -> 返回系统配置 +``` + +这个接口是服务间内部接口,不是普通前端接口。调用方必须带 HMAC 请求头: + +```http +X-Service-Name: im-workbench +X-Timestamp: +X-Nonce: +X-Signature: +``` + +签名使用的密钥来自 `im_manager` 测试环境 Secret 中的: + +```text +manager-system / im-manager-secrets / IM_CONFIG_API_SECRET +``` + +注意:不要把真实密钥写进 Git 文档或代码。 + +## 2. 为什么 .env 不生效 + +测试环境运行在 Kubernetes Pod 中,服务启动时不会自动读取仓库里的 `.env` 文件。 + +当前代码和启动方式是: + +- `im_workbench` 只读取进程环境变量和 `IM_WORKBENCH_CONFIG` 指向的 JSON 配置文件。 +- `im_manager` 的 entrypoint 使用 Pod 环境变量做 `envsubst` 生成 `config.yaml`。 +- 两个服务都没有 `godotenv.Load()`、`source .env` 或 docker compose `env_file` 逻辑。 + +所以测试环境真正生效的是: + +```text +Kubernetes Secret / Deployment 环境变量 +``` + +不是仓库中的 `.env`。 + +## 3. 当前 Kubernetes 配置关系 + +### im_workbench + +命名空间: + +```text +workbench-system +``` + +Deployment: + +```text +im-workbench +``` + +当前 Pod 里只显式注入: + +```text +IM_WORKBENCH_CONFIG=/etc/im-workbench/workbench.json +``` + +`/etc/im-workbench/workbench.json` 来自: + +```text +workbench-system / im-workbench-secrets / workbench.json +``` + +因此 workbench 的运行时配置要改这个 Secret 里的 `workbench.json`,或在 Deployment 里显式加环境变量。 + +### im_manager + +命名空间: + +```text +manager-system +``` + +Deployment: + +```text +im-manager +``` + +Secret: + +```text +manager-system / im-manager-secrets +``` + +其中和内部配置接口相关的 key: + +```text +IM_CONFIG_API_SERVICE_NAME +IM_CONFIG_API_SECRET +``` + +## 4. 必须配置什么 + +### 4.1 im_manager 需要允许 im_workbench 调配置接口 + +`im_manager` 的 `IM_CONFIG_API_SERVICE_NAME` 需要支持两个调用方: + +```text +im-server,im-workbench +``` + +原因: + +- `im_server` 也需要读取 manager 配置。 +- `im_workbench` 也需要读取 manager 配置。 +- 如果只配置 `im-workbench`,会影响 `im_server`。 +- 如果只配置 `im-server`,`im_workbench` 用正确 service name 调用会 401。 + +目标配置: + +```text +IM_CONFIG_API_SERVICE_NAME=im-server,im-workbench +``` + +`IM_CONFIG_API_SECRET` 保持现有值,不要改成空,也不要泄露到日志或文档。 + +### 4.2 im_workbench 需要配置 manager 连接信息 + +`workbench.json` 中需要有: + +```json +{ + "manager": { + "base_url": "https://admin.118im.com", + "service_name": "im-workbench", + "secret": "<填 manager-system/im-manager-secrets 中的 IM_CONFIG_API_SECRET>", + "uid_key_group": "im", + "uid_key_keys": [ + "uid_encryption_key", + "UID_ENCRYPTION_KEY", + "uid_key" + ], + "timeout_ms": 3000 + } +} +``` + +也可以不用改 JSON,改成 Deployment 环境变量: + +```text +IM_WORKBENCH_MANAGER_BASE_URL=https://admin.118im.com +IM_WORKBENCH_MANAGER_SERVICE_NAME=im-workbench +IM_WORKBENCH_MANAGER_SECRET=<填 manager-system/im-manager-secrets 中的 IM_CONFIG_API_SECRET> +``` + +建议优先统一放到 `workbench.json`,因为当前 workbench 已经通过 Secret 挂载 JSON 配置。 + +## 5. 推荐操作流程 + +### 步骤 1:更新 manager Secret + +将: + +```text +manager-system / im-manager-secrets / IM_CONFIG_API_SERVICE_NAME +``` + +改为: + +```text +im-server,im-workbench +``` + +`IM_CONFIG_API_SECRET` 保持现有值。 + +### 步骤 2:更新 workbench Secret + +从: + +```text +manager-system / im-manager-secrets / IM_CONFIG_API_SECRET +``` + +取值,写入: + +```text +workbench-system / im-workbench-secrets / workbench.json +``` + +目标字段: + +```json +{ + "manager": { + "base_url": "https://admin.118im.com", + "service_name": "im-workbench", + "secret": "" + } +} +``` + +### 步骤 3:重启服务 + +更新 Secret 后需要重启 Pod,确保进程重新读取配置。 + +建议顺序: + +```text +1. 重启 im-manager +2. 重启 im-workbench +``` + +原因: + +- 先让 manager 支持 `im-workbench` service name。 +- 再让 workbench 使用 `im-workbench` service name 去调用 manager。 + +## 6. 验证方法 + +### 6.1 验证 manager 是否接受 im-workbench + +用 `IM_CONFIG_API_SECRET` 对请求签名,请求: + +```text +GET https://admin.118im.com/internal/im-config/third-party/im?keys=uid_encryption_key,UID_ENCRYPTION_KEY,uid_key +X-Service-Name: im-workbench +``` + +期望结果: + +```json +{ + "code": 0, + "data": { + "values": { + "uid_encryption_key": "..." + } + } +} +``` + +如果返回 401,重点检查: + +- `IM_CONFIG_API_SERVICE_NAME` 是否包含 `im-workbench` +- 请求里的 `X-Service-Name` 是否是 `im-workbench` +- `IM_CONFIG_API_SECRET` 是否和 manager 当前 Secret 一致 +- 签名文本是否为: + +```text +METHOD +RequestURI +BodySHA256 +Timestamp +Nonce +``` + +### 6.2 验证 workbench 是否加载到 manager 配置 + +进入 workbench Pod 看环境和配置摘要,注意不要打印真实 secret: + +```bash +kubectl -n workbench-system exec -- sh -c 'env | grep IM_WORKBENCH_MANAGER' +``` + +或查看挂载配置: + +```bash +kubectl -n workbench-system exec -- sh -c 'cat /etc/im-workbench/workbench.json' +``` + +检查: + +```text +manager.base_url = https://admin.118im.com +manager.service_name = im-workbench +manager.secret 非空 +``` + +### 6.3 验证业务接口 + +从 IM 登录获取 token 后,请求 workbench 接口: + +```http +GET https://api.118im.com/v1/workbench +Authorization: Bearer +X-MeetingIM-APIKey: +X-IM-Schema: 1 +X-Tenant-Code: +``` + +期望: + +```json +{ + "code": 0 +} +``` + +再创建会议,确认 user id 编解码和展示名正常: + +```http +POST https://api.118im.com/v1/workbench/meetings +Idempotency-Key: +Authorization: Bearer +X-MeetingIM-APIKey: +X-IM-Schema: 1 +X-Tenant-Code: +Content-Type: application/json +``` + +请求体示例: + +```json +{ + "title": "运维验证会议", + "start_at": "2026-09-18T08:00:00Z", + "end_at": "2026-09-18T09:00:00Z", + "location": "", + "recorder_user_id": "usr...", + "attendee_user_ids": ["usr..."], + "reminder_minutes": [] +} +``` + +期望: + +```json +{ + "code": 0, + "data": { + "state": "scheduled", + "organizer": { + "user_id": "usr...", + "display_name": "..." + }, + "participant_names": ["..."] + } +} +``` + +## 7. 常见问题 + +### 7.1 为什么不能从 manager 读取 manager secret + +因为 `IM_CONFIG_API_SECRET` 是调用 manager 配置接口的 HMAC 引导凭证。 + +如果要读取 manager 配置接口,必须先有这个 secret 才能签名。 +所以它不能从同一个 manager 配置接口中获取,只能来自部署环境,例如 Kubernetes Secret。 + +### 7.2 为什么 service name 不是 im-server + +`X-Service-Name` 表示调用方服务名。 + +- `im_server` 调 manager 时,应使用 `im-server` +- `im_workbench` 调 manager 时,应使用 `im-workbench` + +manager 侧应该允许多个合法调用方: + +```text +im-server,im-workbench +``` + +### 7.3 为什么配置了 .env 还是没用 + +因为 Kubernetes 不会自动读取仓库 `.env`。 + +`.env` 只有在以下情况才会生效: + +- 程序代码主动 `godotenv.Load()` +- 启动脚本 `source .env` +- docker compose 配了 `env_file` +- CI/CD 将 `.env` 内容写进 Deployment 或 Secret + +当前测试环境都不是这些方式。 + +### 7.4 只改 workbench 会怎样 + +如果只把 workbench 改成: + +```text +X-Service-Name: im-workbench +``` + +但 manager 仍然只允许: + +```text +IM_CONFIG_API_SERVICE_NAME=im-server +``` + +那么 workbench 调 manager 会返回 401。 + +所以需要 manager 和 workbench 一起配置。 + +## 8. 最终配置清单 + +manager: + +```text +Namespace: manager-system +Secret: im-manager-secrets + +IM_CONFIG_API_SERVICE_NAME=im-server,im-workbench +IM_CONFIG_API_SECRET=<保持现有值> +``` + +workbench: + +```text +Namespace: workbench-system +Secret: im-workbench-secrets +Key: workbench.json +``` + +`workbench.json`: + +```json +{ + "manager": { + "base_url": "https://admin.118im.com", + "service_name": "im-workbench", + "secret": "", + "uid_key_group": "im", + "uid_key_keys": [ + "uid_encryption_key", + "UID_ENCRYPTION_KEY", + "uid_key" + ], + "timeout_ms": 3000 + } +} +``` + diff --git a/internal/config/config.go b/internal/config/config.go index 48c57d1..95c29a0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -105,7 +105,7 @@ func defaultConfig() *Config { RequiredAPIKey: true, }, Manager: ManagerConfig{ - ServiceName: "im-server", + ServiceName: "im-workbench", UIDKeyGroup: "im", UIDKeyKeys: []string{"uid_encryption_key", "UID_ENCRYPTION_KEY", "uid_key"}, TimeoutMS: 3000, diff --git a/internal/managerconfig/client.go b/internal/managerconfig/client.go index a414b8d..331e063 100644 --- a/internal/managerconfig/client.go +++ b/internal/managerconfig/client.go @@ -114,7 +114,7 @@ func (c *Client) newRequest(ctx context.Context, method string, rawURL string, b _, _ = mac.Write([]byte(signingText)) serviceName := strings.TrimSpace(c.cfg.ServiceName) if serviceName == "" { - serviceName = "im-server" + serviceName = "im-workbench" } req.Header.Set("Accept", "application/json") req.Header.Set("X-Service-Name", serviceName) diff --git a/internal/managerconfig/client_test.go b/internal/managerconfig/client_test.go index 3be78d3..0fd553f 100644 --- a/internal/managerconfig/client_test.go +++ b/internal/managerconfig/client_test.go @@ -24,7 +24,7 @@ func TestResolveUIDKeyPrefersManager(t *testing.T) { if r.URL.Query().Get("keys") != "uid_encryption_key,UID_ENCRYPTION_KEY,uid_key" { t.Fatalf("unexpected keys %s", r.URL.Query().Get("keys")) } - if r.Header.Get("X-Service-Name") != "im-server" { + if r.Header.Get("X-Service-Name") != "im-workbench" { t.Fatalf("unexpected service %s", r.Header.Get("X-Service-Name")) } if !validSignature(t, r, secret) { @@ -36,7 +36,7 @@ func TestResolveUIDKeyPrefersManager(t *testing.T) { got := ResolveUIDKey(context.Background(), config.ManagerConfig{ BaseURL: server.URL, - ServiceName: "im-server", + ServiceName: "im-workbench", Secret: secret, UIDKeyGroup: "im", UIDKeyKeys: []string{"uid_encryption_key", "UID_ENCRYPTION_KEY", "uid_key"},