iot_cloud/docs/container_deploy_request.md
2026-05-10 23:44:35 +08:00

19 KiB
Raw Blame History

/container/deploy 容器创建请求参数说明

本文档说明 iot HTTP 接口 POST /container/deploy 当前支持的 JSON 参数、校验规则,以及服务端把 JSON 解码成 Erlang map 后如何转换成下发给 efka 的容器部署命令。

对应代码:

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 重启策略,例如 noalwaysunless-stoppedon-failureon-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}}

其中 Refcrypto:strong_rand_bytes(16) 生成的 16 字节 binary。网络帧只使用 safe term协议 label、command map key 和 action 使用 binaryefka 收到后直接按 binary key/action 处理。

HTTP handler 最多等待 10 秒。超时返回 HTTP 504其他参数或执行错误通常返回 HTTP 400找不到 host 返回业务错误 code 404。

7. Docker create config 映射

create.configbuild_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_configbuild_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_configbuild_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 binaryhandler 会尝试解码后再放入 result

错误示例:

{
  "error": {
    "code": 400,
    "message": "invalid port binding"
  }
}

常见状态:

HTTP 状态 场景
200 host not found 或构造阶段返回的业务错误也可能通过 200 包装业务错误。
400 参数校验失败、efka 返回普通错误。
504 等待 efka command response 超时。