19 KiB
/container/deploy 容器创建请求参数说明
本文档说明 iot HTTP 接口 POST /container/deploy 当前支持的 JSON 参数、校验规则,以及服务端把 JSON 解码成 Erlang map 后如何转换成下发给 efka 的容器部署命令。
对应代码:
- HTTP 入口:src/transport/http/container_handler.erl
- HTTP JSON 解析:src/transport/http/http_protocol.erl
- 参数校验与内部 map 构造:src/docker/docker_container_builder.erl
- 主机命令下发:src/host/iot_host.erl
1. 接口
POST /container/deploy
Content-Type: application/json
HTTP body 必须是 JSON object。http_protocol 会使用 json:decode/1 解析请求体:
- JSON object -> Erlang map
- JSON array -> Erlang list
- JSON string -> Erlang binary
- JSON integer -> Erlang integer
- JSON float -> Erlang float
- JSON boolean -> Erlang
true | false
/container/deploy handler 只接受顶层 map,且必须匹配:
#{<<"uuid">> := UUID, <<"task_id">> := TaskId, <<"config">> := Config}
其中:
UUID必须是 binary,也就是 JSON string。TaskId必须是 integer。Config必须是 map,也就是 JSON object。
2. 完整 JSON 格式
下面示例包含当前支持的所有字段。实际请求可以只传必填字段和需要的可选字段。
{
"uuid": "qbxmjyzrkpntfgswaevodhluicqzxplkm",
"task_id": 1001,
"config": {
"image": "docker.io/library/nginx:latest",
"container_name": "my_nginx",
"command": ["nginx", "-g", "daemon off;"],
"restart": "always",
"entrypoint": ["/docker-entrypoint.sh"],
"envs": ["ENV=prod", "TZ=Asia/Shanghai"],
"expose": ["80", "443/tcp", "53/udp"],
"volumes": ["/host/data:/data", "/host/log:/var/log:ro"],
"networks": ["bridge"],
"network_mode": "bridge",
"labels": {
"app": "nginx",
"env": "prod"
},
"user": "www-data",
"working_dir": "/app",
"hostname": "myhost",
"privileged": false,
"cap_add": ["NET_ADMIN"],
"cap_drop": ["MKNOD"],
"devices": ["/dev/ttyUSB0:/dev/ttyUSB0:rwm"],
"mem_limit": "512m",
"mem_reservation": "256m",
"cpu_shares": 512,
"cpus": 1.5,
"ulimits": {
"nofile": "1024:2048"
},
"sysctls": {
"net.ipv4.ip_forward": "1"
},
"tmpfs": ["/tmp", "/run:rw,size=64m"],
"extra_hosts": ["host.docker.internal:host-gateway"],
"healthcheck": {
"test": ["CMD-SHELL", "curl -f http://localhost || exit 1"],
"interval": "30s",
"timeout": "10s",
"retries": 3
}
}
}
3. 顶层参数
| 字段 | JSON 类型 | 必填 | 说明 |
|---|---|---|---|
uuid |
string | 是 | 目标 efka 所属主机 UUID。服务端用它查找 iot_host 进程,不会下发到 efka。 |
task_id |
non-negative integer | 是 | 部署任务 ID。会进入内部命令的 task_id 字段,用于关联部署结果和部署日志流。iot 内部使用 {uuid, task_id} 作为任务唯一标识。 |
config |
object | 是 | 容器创建配置。会被校验并转换成内部部署参数 map。 |
顶层没有 timeout 字段。当前 HTTP handler 等待 efka command response 的超时时间固定为 10 秒。
部署过程的实时反馈通过 SSE 读取:
GET /event_stream?uuid=<host_uuid>&task_id=<task_id>
/container/deploy 收到合法请求后会先在 iot 内部创建或复用 {uuid, task_id} 对应的任务进程,然后再向 efka 下发部署命令。SSE handler 订阅同一个任务进程;该进程会缓存最近的部署事件,支持多个页面同时订阅,并在收到 efka 的 close 事件后关闭 SSE。
4. config 参数
必填字段
| 字段 | JSON 类型 | 内部类型 | 说明 |
|---|---|---|---|
image |
string | binary | 镜像名。没有 tag 时 efka 部署逻辑会补 :latest。 |
container_name |
string | binary | 容器名称。 |
command |
string[] | binary list | 容器启动命令,对应 Docker create config 的 cmd。 |
restart |
string | binary | 重启策略,例如 no、always、unless-stopped、on-failure、on-failure:3。 |
可选字段
| 字段 | JSON 类型 | 默认值 | 说明 |
|---|---|---|---|
entrypoint |
string[] | [] |
Docker create config 的 entrypoint。 |
envs |
string[] | [] |
环境变量列表,例如 ["A=1"],对应 Docker create config 的 env。 |
expose |
string[] | [] |
容器暴露端口,只表示容器端口,不绑定宿主机端口。 |
ports |
string[] | [] |
宿主机到容器的端口映射,格式为 host_port:container_port,例如 ["8080:80", "443:443"]。 |
volumes |
string[] | [] |
volume bind 列表,格式见后文。 |
networks |
string[] | [] |
Docker network 名称列表,用于 networking config。 |
network_mode |
string | "" |
Docker host config 的 network_mode。 |
labels |
object string:string | {} |
容器 labels。key/value 都必须是 string。 |
user |
string | "" |
容器运行用户。 |
working_dir |
string | "" |
容器工作目录。 |
hostname |
string | "" |
容器 hostname。 |
privileged |
boolean | false |
是否 privileged。 |
cap_add |
string[] | [] |
追加 Linux capability。 |
cap_drop |
string[] | [] |
删除 Linux capability。 |
devices |
string[] | [] |
设备映射列表,格式见后文。 |
mem_limit |
string | 0 |
内存上限,解析成字节数。 |
mem_reservation |
string | 0 |
内存软限制,解析成字节数。 |
cpu_shares |
non-negative integer | 0 |
Docker CPU shares。 |
cpus |
non-negative number | 0 |
CPU 数量,转换成 nano cpus。 |
ulimits |
object string:string | [] |
ulimit 配置,格式见后文。 |
sysctls |
object string:string | {} |
sysctl 配置。key/value 都必须是 string。 |
tmpfs |
string[] | {} |
tmpfs mount 配置,格式见后文。 |
extra_hosts |
string[] | [] |
额外 hosts,例如 ["host.docker.internal:host-gateway"]。 |
healthcheck |
object | undefined |
健康检查配置,格式见后文。 |
不支持字段
| 字段 | 当前行为 | 说明 |
|---|---|---|
container_dir |
明确拒绝 | 容器目录由 efka 按系统默认规则管理,HTTP 调用方不能指定 efka 主机上的部署目录。 |
env_file |
忽略 | 当前校验不会识别该字段,后续构造 Docker create options 时也不会使用。 |
| 其他未知字段 | 忽略 | 除 container_dir 外,未知字段不会报错,也不会进入内部部署参数。 |
5. 校验规则
校验分为两层。
第一层在 container_handler:
uuid必须是 binary。task_id必须是非负 integer。config必须是 map。
第二层在 docker_container_builder:deploy_request/2:
- 先拒绝不支持的
container_dir字段。 - 检查必填字段是否存在。
- 检查已知字段类型。
- 检查
healthcheck内部字段类型,避免无效嵌套字段进入构造阶段。 - 构造内部部署 map。
- 对需要解析的字段做格式解析,例如端口、volume、size、duration、ulimit。
类型错误会返回类似:
required parameter: <<"command">>, type must be: list of string
optional parameter: <<"labels">>, type must be: map of string:string
缺少必填字段会返回类似:
miss requied parameter: <<"image">>
注意:错误文本中的 requied 是当前代码里的原始拼写。
6. 内部转换结果
config 校验通过后,docker_container_builder:deploy_request(TaskId, Config) 生成:
#{
<<"action">> => <<"deploy">>,
<<"task_id">> => TaskId,
<<"params">> => #{
<<"container_name">> => ContainerName,
<<"create">> => #{
<<"config">> => ContainerConfig,
<<"host_config">> => HostConfig,
<<"networking_config">> => NetworkingConfig
}
}
}
该 map 会通过 efka/iot 长连接协议下发:
{<<"command">>, Ref, {<<"container">>, #{
<<"action">> => <<"deploy">>,
<<"task_id">> => TaskId,
<<"params">> => Params
}}}
efka 返回:
{<<"command_response">>, Ref, {<<"container">>, Reply}}
其中 Ref 是 crypto:strong_rand_bytes(16) 生成的 16 字节 binary。网络帧只使用 safe term,协议 label、command map key 和 action 使用 binary;efka 收到后直接按 binary key/action 处理。
HTTP handler 最多等待 10 秒。超时返回 HTTP 504,其他参数或执行错误通常返回 HTTP 400;找不到 host 返回业务错误 code 404。
7. Docker create config 映射
create.config 由 build_docker_container_config/1 生成:
| 内部字段 | 来源 JSON 字段 | 转换规则 |
|---|---|---|
image |
image |
原值。 |
cmd |
command |
原值。 |
entrypoint |
entrypoint |
默认 []。 |
env |
envs |
默认 []。 |
labels |
labels |
默认 {}。 |
volumes |
volumes |
只保留 container path 列表。 |
user |
user |
默认 ""。 |
working_dir |
working_dir |
默认 ""。 |
hostname |
hostname |
默认 ""。 |
exposed_ports |
expose + ports |
转成 #{container_port, protocol} map 列表;ports 中的容器端口会自动补进 exposed_ports。 |
healthcheck |
healthcheck |
未传时为 undefined。 |
8. Docker host config 映射
create.host_config 由 build_docker_host_config/1 生成:
| 内部字段 | 来源 JSON 字段 | 转换规则 |
|---|---|---|
binds |
volumes |
转成 Docker bind 字符串列表。 |
network_mode |
network_mode |
默认 ""。 |
restart_policy |
restart |
转成 #{name, maximum_retry_count}。 |
privileged |
privileged |
默认 false。 |
cap_add |
cap_add |
默认 []。 |
cap_drop |
cap_drop |
默认 []。 |
devices |
devices |
转成设备映射 map 列表。 |
memory |
mem_limit |
解析成字节数,未传为 0。 |
memory_reservation |
mem_reservation |
解析成字节数,未传为 0。 |
nano_cpus |
cpus |
cpus * 1000000000,未传为 0。 |
cpu_shares |
cpu_shares |
未传为 0。 |
port_bindings |
ports |
转成 #{host_ip, host_port, container_port, protocol} map 列表。 |
ulimits |
ulimits |
转成 ulimit map 列表。 |
tmpfs |
tmpfs |
转成 map。 |
sysctls |
sysctls |
默认 {}。 |
extra_hosts |
extra_hosts |
默认 []。 |
9. Docker networking config 映射
create.networking_config 由 build_docker_networking_config/1 生成:
#{<<"endpoints">> => [#{<<"name">> => Network} || Network <- Networks]}
来源字段:
"networks": ["bridge", "mynet"]
转换结果:
#{<<"endpoints">> => [
#{<<"name">> => <<"bridge">>},
#{<<"name">> => <<"mynet">>}
]}
10. 复杂字段转换规则
restart
输入:
"restart": "always"
转换:
#{<<"name">> => <<"always">>, <<"maximum_retry_count">> => 0}
输入:
"restart": "on-failure:3"
转换:
#{<<"name">> => <<"on-failure">>, <<"maximum_retry_count">> => 3}
expose
输入:
["80", "443/tcp", "53/udp"]
转换:
[
#{<<"container_port">> => 80, <<"protocol">> => <<"tcp">>},
#{<<"container_port">> => 443, <<"protocol">> => <<"tcp">>},
#{<<"container_port">> => 53, <<"protocol">> => <<"udp">>}
]
端口必须是无符号整数,且不能超过 4294967295。
ports
输入:
["8080:80", "443:443", "8053:53/udp"]
转换为 create.host_config.port_bindings:
[
#{<<"host_ip">> => <<>>, <<"host_port">> => 8080, <<"container_port">> => 80, <<"protocol">> => <<"tcp">>},
#{<<"host_ip">> => <<>>, <<"host_port">> => 443, <<"container_port">> => 443, <<"protocol">> => <<"tcp">>},
#{<<"host_ip">> => <<>>, <<"host_port">> => 8053, <<"container_port">> => 53, <<"protocol">> => <<"udp">>}
]
规则:
- 端口映射之间使用冒号分隔:
host_port:container_port。 - 容器端口可以带协议:
host_port:container_port/protocol。 - 未指定协议时默认为
tcp。 - host port 和 container port 都不能为空,且必须是
0..65535范围内的无符号整数。 ports中出现的容器端口会自动补进create.config.exposed_ports,因此不需要在expose里重复声明。
volumes
输入:
["/host/data:/data", "/host/log:/var/log:ro", "/host/cache:/cache:rw"]
转换为 create.config.volumes:
[<<"/data">>, <<"/var/log">>, <<"/cache">>]
转换为 create.host_config.binds:
[
<<"/host/data:/data">>,
<<"/host/log:/var/log:ro">>,
<<"/host/cache:/cache">>
]
规则:
host_path:container_path-> 读写挂载。host_path:container_path:ro-> 只读挂载。host_path:container_path:rw-> 当前实现视为读写挂载,输出时不会保留:rw。- host path 和 container path 都不能为空。
devices
输入:
["/dev/ttyUSB0:/dev/ttyUSB0", "/dev/snd:/dev/snd:rwm"]
转换:
[
#{
<<"path_on_host">> => <<"/dev/ttyUSB0">>,
<<"path_in_container">> => <<"/dev/ttyUSB0">>,
<<"cgroup_permissions">> => <<"rwm">>
},
#{
<<"path_on_host">> => <<"/dev/snd">>,
<<"path_in_container">> => <<"/dev/snd">>,
<<"cgroup_permissions">> => <<"rwm">>
}
]
规则:
host_path:container_path-> 权限默认为rwm。host_path:container_path:permissions-> 使用第三段作为权限。- 任意路径或 permissions 为空时返回
invalid device mapping。
ulimits
输入:
{
"nofile": "1024:2048",
"nproc": "4096"
}
转换:
[
#{<<"name">> => <<"nofile">>, <<"soft">> => 1024, <<"hard">> => 2048},
#{<<"name">> => <<"nproc">>, <<"soft">> => 4096, <<"hard">> => 4096}
]
规则:
"soft:hard"-> 分别设置 soft 和 hard。"limit"-> soft 和 hard 都等于 limit。- 数值必须是无符号整数。
tmpfs
输入:
["/tmp", "/run:rw,size=64m"]
转换:
#{
<<"/tmp">> => <<>>,
<<"/run">> => <<"rw,size=64m">>
}
规则:
"path"-> options 为""。"path:options"-> options 为第二段。- path 不能为空。
healthcheck
输入:
{
"test": ["CMD-SHELL", "curl -f http://localhost || exit 1"],
"interval": "30s",
"timeout": "10s",
"retries": 3
}
转换:
#{
<<"test">> => [<<"CMD-SHELL">>, <<"curl -f http://localhost || exit 1">>],
<<"interval_ns">> => 30000000000,
<<"timeout_ns">> => 10000000000,
<<"retries">> => 3
}
字段规则:
| 字段 | JSON 类型 | 默认值 | 说明 |
|---|---|---|---|
test |
string[] | [] |
Docker healthcheck test 参数列表。 |
interval |
string 或 non-negative integer | "0s" |
string 会解析时间单位;integer 直接视为纳秒。 |
timeout |
string 或 non-negative integer | "0s" |
string 会解析时间单位;integer 直接视为纳秒。 |
retries |
non-negative integer | 0 |
Docker healthcheck 重试次数。 |
时间单位:
| 单位 | 含义 |
|---|---|
ns |
纳秒 |
us |
微秒 |
ms |
毫秒 |
s |
秒 |
m |
分钟 |
h |
小时 |
| 无单位 | 秒 |
示例:
"30s"->30000000000"10ms"->10000000"2m"->120000000000"5"->5000000000
mem_limit 和 mem_reservation
输入:
"mem_limit": "512m",
"mem_reservation": "1g"
转换:
<<"memory">> => 536870912,
<<"memory_reservation">> => 1073741824
支持单位:
| 单位 | 倍数 |
|---|---|
b 或无单位 |
1 |
k, kb, ki, kib |
1024 |
m, mb, mi, mib |
1048576 |
g, gb, gi, gib |
1073741824 |
t, tb, ti, tib |
1099511627776 |
数值支持整数或小数,例如 "1.5g"。
cpus
输入:
"cpus": 1.5
转换:
<<"nano_cpus">> => 1500000000
规则:
- integer 或 float 都可以。
- 必须大于等于 0。
- 转换公式:
trunc(Cpus * 1000000000)。
11. 最小请求示例
{
"uuid": "qbxmjyzrkpntfgswaevodhluicqzxplkm",
"task_id": 1001,
"config": {
"image": "docker.io/library/nginx:latest",
"container_name": "my_nginx",
"command": ["nginx", "-g", "daemon off;"],
"restart": "always"
}
}
对应下发给 efka 的协议命令示意:
{<<"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;">>],
<<"entrypoint">> => [],
<<"env">> => [],
<<"labels">> => #{},
<<"volumes">> => [],
<<"user">> => <<>>,
<<"working_dir">> => <<>>,
<<"hostname">> => <<>>,
<<"exposed_ports">> => [],
<<"healthcheck">> => undefined
},
<<"host_config">> => #{
<<"binds">> => [],
<<"network_mode">> => <<>>,
<<"restart_policy">> => #{<<"name">> => <<"always">>, <<"maximum_retry_count">> => 0},
<<"privileged">> => false,
<<"cap_add">> => [],
<<"cap_drop">> => [],
<<"devices">> => [],
<<"memory">> => 0,
<<"memory_reservation">> => 0,
<<"nano_cpus">> => 0,
<<"cpu_shares">> => 0,
<<"port_bindings">> => [],
<<"ulimits">> => [],
<<"tmpfs">> => #{},
<<"sysctls">> => #{},
<<"extra_hosts">> => []
},
<<"networking_config">> => #{<<"endpoints">> => []}
}
}
}}}
12. 响应
成功时 HTTP body 由 iot_util:json_data/1 包装:
{
"result": "ok"
}
如果 efka 返回的是 JSON binary,handler 会尝试解码后再放入 result。
错误示例:
{
"error": {
"code": 400,
"message": "invalid port binding"
}
}
常见状态:
| HTTP 状态 | 场景 |
|---|---|
200 |
host not found 或构造阶段返回的业务错误也可能通过 200 包装业务错误。 |
400 |
参数校验失败、efka 返回普通错误。 |
504 |
等待 efka command response 超时。 |