15 KiB
IOT 容器命令到 Docker JSON 的转换说明
本文档描述 efka 收到 iot 下发的容器管理 command 后,接受的 Erlang map 格式,以及 deploy 命令中 create 参数如何转换成 Docker Engine API 接收的 JSON。
对应代码:
- command 接收入口:src/transport/efka_client.erl
- 部署任务管理:src/docker/docker_deploy_manager.erl
- 部署执行:src/docker/docker_deployer.erl
- Docker JSON 构造:src/docker/docker_container_builder.erl
- Docker API 调用:src/docker/docker_commands.erl
1. 协议入口
iot 通过 TLS 长连接向 efka 下发容器命令:
{<<"command">>, Ref, {<<"container">>, CommandMap}}
efka 执行后回复:
{<<"command_response">>, Ref, {<<"container">>, Reply}}
Reply 取值:
<<"ok">>
{<<"ok">>, Result}
{<<"error">>, Reason}
Ref 是 crypto:strong_rand_bytes(16) 生成的 16 字节 binary。网络帧只使用 binary_to_term(PacketBin, [safe]) 可解码的 safe term;协议 label、业务 label、map key 和 action 都使用 binary。
只有 efka_client 处于 activated 状态时,容器命令才会正常执行;处于非 activated 状态时会返回错误。
2. 容器命令 map
efka_client 收到网络协议里的 binary-key CommandMap 后,直接按 binary key 和 binary action 做函数参数匹配,不再做整包 atom-key 转换。
- command 顶层、
target、deployparams、create中间结构都使用 binary key。 <<"action">>的取值使用 binary:<<"list">>、<<"deploy">>、<<"start">>、<<"stop">>、<<"kill">>、<<"remove">>、<<"config">>。undefined作为 Erlang 已有 atom 可以直接出现在 safe term 中;协议不再使用额外的哨兵 binary 表示缺省值。
下面各小节描述的是 efka 直接接收和处理的 map 格式。
list
#{<<"action">> => <<"list">>}
执行:
docker_commands:get_containers()
Docker API:
GET /containers/json?all=true
deploy
#{
<<"action">> => <<"deploy">>,
<<"task_id">> => TaskId,
<<"params">> => Params
}
执行:
docker_deploy_manager:deploy(TaskId, Params)
deploy/2 会启动独立部署进程,HTTP command response 只表示部署任务是否成功启动。实际部署过程和结果通过 task_event 消息流上报给 iot。
start
#{
<<"action">> => <<"start">>,
<<"target">> => Target
}
执行:
docker_commands:start_container(ContainerNameOrId)
Docker API:
POST /containers/{name_or_id}/start
stop
#{
<<"action">> => <<"stop">>,
<<"target">> => Target,
<<"timeout_seconds">> => TimeoutSeconds
}
执行:
docker_commands:stop_container(ContainerNameOrId, TimeoutSeconds)
Docker API:
POST /containers/{name_or_id}/stop?t={TimeoutSeconds}
kill
#{
<<"action">> => <<"kill">>,
<<"target">> => Target,
<<"signal">> => Signal
}
执行:
docker_commands:kill_container(ContainerNameOrId, Signal)
remove
#{
<<"action">> => <<"remove">>,
<<"target">> => Target,
<<"force">> => Force,
<<"remove_volumes">> => RemoveVolumes
}
执行:
docker_commands:remove_container(ContainerNameOrId, Force, RemoveVolumes)
config
#{
<<"action">> => <<"config">>,
<<"target">> => Target,
<<"config">> => Config
}
执行:
docker_helper:update_container_config(ContainerNameOrId, iolist_to_binary(Config))
该命令不会调用 Docker API,只会更新 efka 主机上对应容器目录里的 service.conf。
3. Target 解析规则
target 是 map:
#{
<<"name">> => ContainerName,
<<"id">> => ContainerId
}
解析规则:
- 优先使用
name。 name为空时使用id。name和id都为空时会触发匹配错误。
4. deploy params 格式
deploy 的 params 必须包含:
#{
<<"container_name">> => ContainerName,
<<"create">> => Create
}
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
container_name |
binary | 容器名称。用于容器创建 URL 的 name 参数,也会注入环境变量 CONTAINER_NAME。 |
create |
map | Docker create options 的中间结构,由 iot 侧构造,efka 侧补丁后转为 Docker JSON。 |
docker_deploy_manager 会根据 container_name 按系统默认规则确保容器目录存在。目录固定为:
efka.root_dir/container_name/
即使旧客户端在 params 中携带 container_dir,当前 deploy 流程也会忽略它,避免 HTTP 调用方控制 efka 主机上的写入目录。
5. create 中间结构
create 的结构:
#{
<<"config">> => ContainerConfig,
<<"host_config">> => HostConfig,
<<"networking_config">> => NetworkingConfig
}
这三个 map 会被 docker_container_builder:build_options/3 转成 Docker Engine API JSON。
6. efka 自动补丁规则
在转 Docker JSON 前,efka 会先执行补丁:
环境变量
向 create.config.env 前置注入:
<<"CONTAINER_NAME=", ContainerName/binary>>
如果原 env 列表已经包含完全相同的值,则不会重复添加。
配置文件 volume
向 create.config.volumes 前置注入容器内配置路径:
<<"/usr/local/etc/service.conf">>
配置文件 bind
向 create.host_config.binds 前置注入宿主机配置文件映射:
<<ConfigFile/binary, ":/usr/local/etc/service.conf">>
其中 ConfigFile 是当前容器目录下的:
service.conf
如果列表里已存在完全相同的 bind,则不会重复添加。
7. Docker JSON 顶层结构
docker_commands:create_container/2 最终调用:
POST /containers/create?name={container_name}
Content-Type: application/json
请求 body 来自:
Options = docker_container_builder:build_options(ContainerName, ContainerDir, Create),
Body = iolist_to_binary(json:encode(Options))
最终 JSON 顶层字段:
{
"Image": "...",
"Cmd": [],
"Entrypoint": [],
"Env": [],
"Labels": {},
"Volumes": {},
"User": "",
"WorkingDir": "",
"Hostname": "",
"ExposedPorts": {},
"NetworkingConfig": {},
"Healthcheck": {},
"HostConfig": {}
}
空列表或空 map 字段有的会保留,有的会被省略到 {},具体见下面映射规则。
8. create.config 到 Docker JSON
| 中间字段 | Docker JSON 字段 | 转换规则 |
|---|---|---|
image |
Image |
转成 binary;缺省为 ""。部署流程拉镜像时会对 image 补 :latest,但 create JSON 使用传入值。 |
cmd |
Cmd |
列表元素逐个转 binary。 |
entrypoint |
Entrypoint |
列表元素逐个转 binary。 |
env |
Env |
列表元素逐个转 binary,并自动注入 CONTAINER_NAME=...。 |
labels |
Labels |
key/value 都转 binary。 |
volumes |
Volumes |
转成 Docker 要求的 object,key 是容器内路径,value 是 {}。 |
user |
User |
转成 binary;缺省为 ""。 |
working_dir |
WorkingDir |
转成 binary;缺省为 ""。 |
hostname |
Hostname |
转成 binary;缺省为 ""。 |
exposed_ports |
ExposedPorts |
转成 Docker 端口 object。 |
healthcheck |
Healthcheck |
转成 Docker Healthcheck object;未传时为 {}。 |
Volumes
输入:
[<<"/data">>, <<"/usr/local/etc/service.conf">>]
输出 JSON:
{
"Volumes": {
"/data": {},
"/usr/local/etc/service.conf": {}
}
}
ExposedPorts
输入:
[
#{<<"container_port">> => 80, <<"protocol">> => <<"tcp">>},
#{<<"container_port">> => 53, <<"protocol">> => <<"udp">>}
]
输出 JSON:
{
"ExposedPorts": {
"80/tcp": {},
"53/udp": {}
}
}
规则:
protocol为空或<<"tcp">>时输出tcp。- 其他 protocol 原样输出。
Healthcheck
该字段由 iot 校验后下发。test 必须是 binary list,interval_ns、timeout_ns 和 retries 必须是非负整数;efka 只按收到的中间 map 转成 Docker JSON 字段。
输入:
#{
<<"test">> => [<<"CMD-SHELL">>, <<"curl -f http://localhost || exit 1">>],
<<"interval_ns">> => 30000000000,
<<"timeout_ns">> => 10000000000,
<<"retries">> => 3
}
输出 JSON:
{
"Healthcheck": {
"Test": ["CMD-SHELL", "curl -f http://localhost || exit 1"],
"Interval": 30000000000,
"Timeout": 10000000000,
"Retries": 3
}
}
9. create.host_config 到 Docker JSON
host_config 会被合并到 Docker JSON 的 HostConfig 字段。
| 中间字段 | Docker JSON 字段 | 转换规则 |
|---|---|---|
binds |
HostConfig.Binds |
列表元素逐个转 binary,并自动注入 service.conf bind。空列表时省略。 |
network_mode |
HostConfig.NetworkMode |
空 binary 时省略。 |
restart_policy |
HostConfig.RestartPolicy |
转成 Name 和可选 MaximumRetryCount。 |
privileged |
HostConfig.Privileged |
只有 true 时输出。false 时省略。 |
cap_add / cap_drop |
HostConfig.CapAdd / HostConfig.CapDrop |
两者都为空时省略;只要一个非空,两个字段都会输出。 |
devices |
HostConfig.Devices |
转成 Docker device object 列表。 |
memory |
HostConfig.Memory |
0 时省略。 |
memory_reservation |
HostConfig.MemoryReservation |
0 时省略。 |
nano_cpus |
HostConfig.NanoCpus |
0 时省略。 |
cpu_shares |
HostConfig.CpuShares |
0 时省略。 |
ulimits |
HostConfig.Ulimits |
空列表时省略。 |
tmpfs |
HostConfig.Tmpfs |
空 map 时省略。 |
sysctls |
HostConfig.Sysctls |
空 map 时省略。 |
extra_hosts |
HostConfig.ExtraHosts |
空列表时省略。 |
Binds
输入:
[
<<"/host/data:/data">>,
<<"/host/log:/var/log:ro">>
]
efka 自动补丁后输出:
{
"HostConfig": {
"Binds": [
"/path/to/container/service.conf:/usr/local/etc/service.conf",
"/host/data:/data",
"/host/log:/var/log:ro"
]
}
}
RestartPolicy
输入:
#{<<"name">> => <<"always">>, <<"maximum_retry_count">> => 0}
输出:
{
"HostConfig": {
"RestartPolicy": {
"Name": "always"
}
}
}
输入:
#{<<"name">> => <<"on-failure">>, <<"maximum_retry_count">> => 3}
输出:
{
"HostConfig": {
"RestartPolicy": {
"Name": "on-failure",
"MaximumRetryCount": 3
}
}
}
Devices
输入:
[
#{
<<"path_on_host">> => <<"/dev/ttyUSB0">>,
<<"path_in_container">> => <<"/dev/ttyUSB0">>,
<<"cgroup_permissions">> => <<"rwm">>
}
]
输出:
{
"HostConfig": {
"Devices": [
{
"PathOnHost": "/dev/ttyUSB0",
"PathInContainer": "/dev/ttyUSB0",
"CgroupPermissions": "rwm"
}
]
}
}
cgroup_permissions 为空时默认输出 rwm。
资源限制
输入:
#{
<<"memory">> => 536870912,
<<"memory_reservation">> => 268435456,
<<"nano_cpus">> => 1500000000,
<<"cpu_shares">> => 512
}
输出:
{
"HostConfig": {
"Memory": 536870912,
"MemoryReservation": 268435456,
"NanoCpus": 1500000000,
"CpuShares": 512
}
}
值为 0 的资源字段会被省略。
Ulimits
输入:
[
#{<<"name">> => <<"nofile">>, <<"soft">> => 1024, <<"hard">> => 2048}
]
输出:
{
"HostConfig": {
"Ulimits": [
{
"Name": "nofile",
"Soft": 1024,
"Hard": 2048
}
]
}
}
Tmpfs
输入:
#{
<<"/tmp">> => <<>>,
<<"/run">> => <<"rw,size=64m">>
}
输出:
{
"HostConfig": {
"Tmpfs": {
"/tmp": "",
"/run": "rw,size=64m"
}
}
}
Sysctls
输入:
#{<<"net.ipv4.ip_forward">> => <<"1">>}
输出:
{
"HostConfig": {
"Sysctls": {
"net.ipv4.ip_forward": "1"
}
}
}
ExtraHosts
输入:
[<<"host.docker.internal:host-gateway">>]
输出:
{
"HostConfig": {
"ExtraHosts": ["host.docker.internal:host-gateway"]
}
}
10. create.networking_config 到 Docker JSON
输入:
#{
<<"endpoints">> => [
#{<<"name">> => <<"bridge">>},
#{<<"name">> => <<"mynet">>}
]
}
输出:
{
"NetworkingConfig": {
"EndpointsConfig": {
"bridge": {},
"mynet": {}
}
}
}
未传或 endpoints 为空时输出:
{
"NetworkingConfig": {}
}
11. 完整转换示例
收到的 deploy command:
{<<"command">>, Ref, {<<"container">>, #{
<<"action">> => <<"deploy">>,
<<"task_id">> => 1001,
<<"params">> => #{
<<"container_name">> => <<"my_nginx">>,
<<"create">> => #{
<<"config">> => #{
<<"image">> => <<"docker.io/library/nginx:latest">>,
<<"cmd">> => [<<"nginx">>, <<"-g">>, <<"daemon off;">>],
<<"env">> => [<<"ENV=prod">>],
<<"volumes">> => [<<"/data">>],
<<"exposed_ports">> => [#{<<"container_port">> => 80, <<"protocol">> => <<"tcp">>}]
},
<<"host_config">> => #{
<<"binds">> => [<<"/host/data:/data">>],
<<"restart_policy">> => #{<<"name">> => <<"always">>, <<"maximum_retry_count">> => 0},
<<"memory">> => 536870912
},
<<"networking_config">> => #{
<<"endpoints">> => [#{<<"name">> => <<"bridge">>}]
}
}
}
}}}
生成的 Docker JSON 示意:
{
"Image": "docker.io/library/nginx:latest",
"Cmd": ["nginx", "-g", "daemon off;"],
"Entrypoint": [],
"Env": ["CONTAINER_NAME=my_nginx", "ENV=prod"],
"Labels": {},
"Volumes": {
"/usr/local/etc/service.conf": {},
"/data": {}
},
"User": "",
"WorkingDir": "",
"Hostname": "",
"ExposedPorts": {
"80/tcp": {}
},
"NetworkingConfig": {
"EndpointsConfig": {
"bridge": {}
}
},
"Healthcheck": {},
"HostConfig": {
"Binds": [
"/efka/root/my_nginx/service.conf:/usr/local/etc/service.conf",
"/host/data:/data"
],
"RestartPolicy": {
"Name": "always"
},
"Memory": 536870912
}
}
12. 部署流程补充
deploy action 的执行流程:
- 根据
container_name和efka.root_dir确保默认容器目录存在。 - 启动独立部署进程。
- 部署进程上报任务事件流。
- 规范化镜像名:如果镜像最后一段没有 tag,则补
:latest。 - 调用 Docker API 拉取镜像。
- 构造 Docker create JSON。
- 调用
POST /containers/create?name={container_name}。 - 创建空
service.conf文件。 - 写入部署摘要日志并关闭任务事件流。
注意:当前 ensure_container_absent/2 只上报“开始创建容器”,不会删除已有同名容器;如果 Docker 返回名称冲突,部署会失败并上报“本地容器已经存在”。