ServerManager 做的是远程管理节点——浏览器连 Panel,Panel 通过 WebSocket 连 Node,Node 在本地操作 Docker。听起来链路不长,但要把镜像、容器、网络、卷、配置、Compose 六类资源的四十多个操作都可靠地搬上 WebSocket,里面的弯弯绕并不少。
第一个问题:六类资源怎么统一路由?
Docker 的 API 表面上一堆方法,但抽象出来就六类——image、container、network、volume、config、compose。每个操作都是”资源.动作”的组合,比如 docker:image:pull、docker:container:start。
消息到达 Node 的分发器后,按冒号拆分:
1 | |
三段式 docker:resource:operation,第二段是资源类型,第三段是操作名。DockerManager.handle(resource, operation, payload) 收到后分发到 __image、__container 等内部方法。为什么不用一个大的 if-else?因为六类资源的方法各有各的参数校验和返回格式,塞在一个方法里三四百行,拆开每个资源一个方法,代码结构清晰得多。
安全分层:读操作畅通,写操作要令牌
不是所有 Docker 操作都一样危险。docker:image:list 和 docker:container:kill 的安全级别显然不同。
在 dockerManager.py 里,安全检查分层了:
1 | |
总开关 docker_manager 控制能不能用 Docker 功能。如果打开了,读操作(list、inspect、logs 之类)不需要额外权限——让监控数据流出去问题不大。
但写操作要看更细粒度的权限:拉镜像要 docker_image_manage,搞容器要 docker_container_manage,Compose 要……好吧,目前没有单独的 docker_compose_manage,用的是 docker_manager 总开关。不过关键是所有安全检查都在业务逻辑之前,不会出现”先操作了再检查权限”的情况。
RESOURCE_FLAGS 这个映射表也是显式声明的:
1 | |
跟前面那篇 Action Registry 的思路一脉相承——安全策略是数据,不是散落在代码里的字符串常量。
同步转异步:不能把事件循环堵住
Docker 的 Python SDK 是同步的——client.images.list()、client.containers.get() 这些调用都会阻塞。但 Node 端跑的是 asyncio 事件循环,一个 docker.images.pull("python:3.12") 可能要跑几十秒。要是直接在协程里调,整个 WebSocket 的接收循环就卡住了。
解决办法是 asyncio.to_thread:
1 | |
一行把同步调用甩到线程池里,事件循环不阻塞,其他 WebSocket 消息照常接收和处理。to_thread 内部用的是 loop.run_in_executor(None, func),默认线程池大小够用了。
镜像自动拉取:拉还是不拉?
创建容器的时候有个细节——用户指定的镜像本地不存在怎么办?
1 | |
三步走:先查本地有没有,没有的话看 pull_missing 参数(默认 True),再看有没有 docker_image_manage 权限。 Defaults toward 便利(自动拉),但安全策略随时可以拦住。
这里有个容易忽略的问题——pull_missing 默认是 True。如果你没关掉这个选项,创建容器的操作可能会隐式触发一个耗时很长的镜像拉取。在自动化场景下这是利好,但如果网络不好或者镜像仓库连不上,就会有一个请求卡在那很久。docker_manager 没有设全局超时来兜底拉镜像操作,这是个需要改进的点。
Compose 管理:在节点上用 docker compose CLI
Docker Compose 的操作没走 Python SDK——因为 SDK 对 Compose 的支持太弱了。取而代之的是直接调 docker compose 命令行:
1 | |
配置文件存在 data/docker_compose/<project>/compose.yml 下,项目名就是目录名。这里有个路径安全检查:
1 | |
项目名只允许字母数字下划线短横线,然后做路径穿越检查。同样是那条铁律——用户输入的路径不能逃出预设的根目录。
容器执行:docker exec 的远程版
docker:container:exec 是个值得单独说的操作。它允许通过 Panel 在节点上的容器里执行命令:
1 | |
privileged=True 这个选项是故意暴露的——有些运维场景确实需要在特权容器里操作。但 docker:container:exec 是 SIGNED 级别的动作,签名验不过去连接直接断。安全边界不在参数白名单里,而在 HMAC 签名和应用层权限控制里。
Docker 配置管理:写 daemon.json 的正确姿势
docker:config:update 直接改 /etc/docker/daemon.json,听起来很危险。代码里做了几层保护:
1 | |
先备份旧配置(.json.bak),再写入新配置。万一新配置写坏了,至少有个回退点。
reload 和 restart_service 更是通过 subprocess.run 调用 systemctl reload docker 和 systemctl restart docker,这些是标准的服务管理命令,不是什么黑魔法。但它们也需要 docker_config_manage 权限开关开着才能执行。
返回值的 JSON 安全化
Docker SDK 返回的对象经常包含不能直接 JSON 序列化的类型(比如 datetime、Image 对象等)。所有返回到 WebSocket 的数据都要过一遍 JSON 安全化:
1 | |
default=str 是个万能兜底——任何不能序列化的类型都转成字符串。虽然丢失了原始类型信息,但保证了 WebSocket 消息永远不会因为序列化报错而中断。比起精心设计每个类型的序列化器,这种简单粗暴的做法在运维场景下更靠谱——最怕的不是数据格式不完美,而是操作到一半因为序列化炸了。
错误处理的分层
Docker 操作可能出各种错——镜像不存在、容器名冲突、网络被占用、权限不够……DockerManager 的错误处理分了三层:
参数校验层——在调 Docker SDK 之前就拦住明显不合法的输入:
1 | |
__required 支持多个候选 key——container/container_id/id 都能匹配,这在对齐 Panel 和 Node 的数据结构差异时很实用。
Docker SDK 异常层——ImageNotFound、APIError 这些异常会在 handle 方法的外层 try-catch 里被统一捕获,然后通过 __send_error 发回 Panel。
未知异常兜底——任何 Exception 都会被捕获,error 消息发回 Panel,不会导致节点断连或者 WebSocket 消息循环崩溃。
四十条操作的完整画面
把六类资源的操作列出来,就是这么大一张表:
| 资源 | 操作 | 安全 |
|---|---|---|
| image | list, inspect, history, pull, remove, tag, prune | pull/remove/tag/prune 需 docker_image_manage |
| container | list, inspect, stats, logs, create, start, stop, restart, kill, pause, unpause, remove, rename, update, exec | 写操作需 docker_container_manage |
| network | list, inspect, create, remove, connect, disconnect, prune | 写操作需 docker_network_manage |
| volume | list, inspect, create, remove, prune | 写操作需 docker_volume_manage |
| config | get, validate, update, reload, restart_service | 写操作需 docker_config_manage |
| compose | list, save, ps, logs, config, up, down, start, stop, restart, pull, build, rm, delete | 写操作需 docker_manager |
每一个操作都是 WebSocket action → 安全检查 → Docker SDK 调用 → JSON 安全化 → WebSocket response 的标准流程。看起来枯燥,但这种一致性的好处是出了问题能快速定位——任何 Docker 操作失败,查日志就能看到 action、resource、operation、error 四个字段,不用猜。
关于直播室和手术室的关系——WebSocket 是直播室,Docker SDK 是手术室。直播室里消息飞来飞去出了问题可以打断、可以重试,手术室里出了问题就是真出了问题。这中间的缓冲层——安全检查、参数校验、错误回传——每一层都是保护手术室不出事故的双保险。