6.3 KiB
浏览器容器控制面
技术选型
- 前端:React 19 + react-admin(ra-core)+ MUI,包含运行环境、镜像版本、网关管理三个页面。
- 后端:Go 模块化单体,Fiber v3 提供 HTTP 路由,Viper 读取并校验启动配置,Logrus 输出 JSON 结构化日志,Cobra 保持当前两个服务入口。控制面提供同源 API 和静态文件,并编排网关;受限网关单独封装 Docker Engine API,是纯执行器。
- 数据:环境配置(别名、中文名、网关、镜像版本、指纹参数)持久化在 Postgres,运行态实时查询网关;Profile 使用命名卷持久化;阶段 A 账号、凭据引用、确认、任务、尝试和审计实体同样由控制面持久化到 Postgres。
- 部署:Docker Compose 启动控制面和受限网关;浏览器容器由网关按平台下发的镜像引用动态创建,缺失时自动拉取。
调用链与契约
React ──> control-plane ── /api/browsers ──(Bearer token)──> docker-gateway ──> docker.sock
│ │
│ └─> browser container
└─ /api/phase-a, /api/browser-images, /api/gateways ──> PostgreSQL
控制面是唯一事实源:网关不持有镜像清单和业务规则,镜像引用、启动命令和卷名均随请求下发。
POST /api/browsers接受{alias, name, gateway, image_version, fingerprint}(严格 JSON,未知字段拒绝),校验后先落库,再调网关创建并启动;网关失败时回滚数据库行。name为中文环境名,alias限^[a-z0-9][a-z0-9-]{0,31}$,容器名creatorhub-browser-<alias>,Profile 卷creatorhub-profile-<alias>。GET /api/browsers合并数据库环境与网关实时状态;环境在网关无容器时显示为未部署。POST /api/browsers/{alias}/start|stop改变状态;POST /api/browsers/{alias}/upgrade收{version},由平台编排:停止并删除旧容器(保留 Profile 卷)→ 用新镜像引用与原指纹参数重建 → 启动;失败直接重试,不做自动回滚。DELETE /api/browsers/{alias}回收容器并删除数据库行,保留 Profile 数据卷。GET/POST /api/browser-images维护可用镜像版本(版本号不可改,PUT /{version}仅接受image_ref/note/enabled);仅启用版本可用于创建与升级;被环境引用时拒绝删除。GET/POST /api/gateways注册网关(POST可携带令牌,否则平台生成 48 位十六进制令牌并明文存储),DELETE /api/gateways/{name}删除;仍被环境引用时拒绝删除。- 启停接受幂等响应,不自动重试未知结果;别名唯一约束由数据库保证。
docker.sock 安全边界
将 socket 以只读文件挂载不会限制 Docker API 的写操作;拥有 socket 等价于拥有宿主机 root 权限。因此:
- 只有
docker-gateway挂载 socket,控制面和浏览器容器均不可见;网关只加入 control 网络,浏览器不能连接网关; - 网关只暴露面向领域的路由,不提供通用 Docker 代理;
/v1全部接口校验Authorization: Bearer <GATEWAY_TOKEN>(常数时间比较),令牌由部署者在网关环境变量与平台注册表中保持一致; - 网关固定命令、网络、挂载和资源限制;外部输入是受校验的别名,以及平台下发的镜像引用、启动参数和卷名——镜像引用来自平台维护的版本表,新增/变更由人工在页面审核启用,不再写死在代码中;
- 启停和删除前必须同时匹配固定名称前缀及
io.creatorhub.managed、io.creatorhub.runtime-id标签; - 动态容器使用只读根文件系统、非 root
1000:1000与固定镜像入口、全部 capability drop、no-new-privileges、CPU/内存/PID 限制,且无宿主机端口和目录挂载; - 控制面发布到宿主机所有网卡;控制网络为固定名称的 Compose 网络;浏览器 bridge 按 ownership、role、driver、Internal 失败关闭校验,且拒绝复用 control 网络;
- Compose 基础镜像锁定 digest;浏览器镜像推荐使用
@sha256:摘要引用以获得不可变性,tag 引用由部署者自行把控。
网关自身一旦被攻破,socket 仍允许接管宿主机;应用内校验不能消除这个平台级风险。开发阶段控制面不做认证或访问限制,安全由部署者自行把控。
运行
Docker socket 的 GID 因宿主机而异:
export GATEWAY_TOKEN="$(openssl rand -hex 24)" # 亦可在 .env 中设置
DOCKER_GID=$(stat -c %g /var/run/docker.sock) docker compose up --build
打开 http://127.0.0.1:8080,局域网内用宿主机 IP 访问同一端口。首次使用:在「网关管理」用 GATEWAY_TOKEN 注册 http://docker-gateway:8081,在「镜像版本」添加镜像引用(缺失时网关自动拉取,拉取上限 10 分钟)。浏览器容器可访问外网。
创建成功但启动失败时,网关会立即删除失败容器并保留命名 Profile 卷,控制面回滚数据库行,允许同名请求安全重试。
阶段 A 离线闭环
POST /api/phase-a/accounts 只接受 OS Keyring/Secret Manager 的引用标识,不接受秘密值;账号的 profile_id 全局唯一。POST /api/phase-a/runtimes 通过部分唯一索引保证一个账号和一个运行时都只有一条活动绑定。
草稿经 POST /api/phase-a/confirmations 显式确认后才可投递到 /api/phase-a/tasks。任务由幂等键去重;POST /api/phase-a/mock/execute 使用 FOR UPDATE SKIP LOCKED 领取一分钟租约,执行前统一核对账号、草稿和确认版本。缺少确认或版本不一致会进入 needs_confirmation,暂停账号或 Mock 策略结果会进入 policy_hold,不确定结果与过期租约进入 needs_confirmation;这些状态都不会自动重试。GET /api/phase-a/audit 只导出账号、确认版本、尝试和结果等非秘密证据。
启动时控制面在事务和 advisory lock 下应用前向迁移 internal/phasea/migrations/001_phase_a.sql。本迁移只新建表、索引、约束和追加式审计触发器,不删除或改写现有数据;回滚需停服务后人工删除阶段 A 新表,本阶段不提供自动破坏性回滚。