14 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}
只有 efka_client 处于 activated 状态时,容器命令才会正常执行;处于 restricted 或其他状态时会返回错误。
2. 容器命令 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
输入:
#{
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 返回名称冲突,部署会失败并上报“本地容器已经存在”。