632 lines
16 KiB
Markdown
632 lines
16 KiB
Markdown
# /container/deploy 容器创建请求参数说明
|
||
|
||
本文档说明 `iot` HTTP 接口 `POST /container/deploy` 当前支持的 JSON 参数、校验规则,以及服务端把 JSON 解码成 Erlang map 后如何转换成下发给 `efka` 的容器部署命令。
|
||
|
||
对应代码:
|
||
|
||
- HTTP 入口:[src/transport/http/container_handler.erl](/usr/local/code/cloudkit/iot/src/transport/http/container_handler.erl:63)
|
||
- HTTP JSON 解析:[src/transport/http/http_protocol.erl](/usr/local/code/cloudkit/iot/src/transport/http/http_protocol.erl:68)
|
||
- 参数校验与内部 map 构造:[src/docker/docker_container_builder.erl](/usr/local/code/cloudkit/iot/src/docker/docker_container_builder.erl:25)
|
||
- 主机命令下发:[src/host/iot_host.erl](/usr/local/code/cloudkit/iot/src/host/iot_host.erl:99)
|
||
|
||
## 1. 接口
|
||
|
||
```http
|
||
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,且必须匹配:
|
||
|
||
```erlang
|
||
#{<<"uuid">> := UUID, <<"task_id">> := TaskId, <<"config">> := Config}
|
||
```
|
||
|
||
其中:
|
||
|
||
- `UUID` 必须是 binary,也就是 JSON string。
|
||
- `TaskId` 必须是 integer。
|
||
- `Config` 必须是 map,也就是 JSON object。
|
||
|
||
## 2. 完整 JSON 格式
|
||
|
||
下面示例包含当前支持的所有字段。实际请求可以只传必填字段和需要的可选字段。
|
||
|
||
```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` | integer | 是 | 部署任务 ID。会进入内部命令的 `task_id` 字段,用于关联部署结果和部署日志流。 |
|
||
| `config` | object | 是 | 容器创建配置。会被校验并转换成内部部署参数 map。 |
|
||
|
||
顶层没有 `timeout` 字段。当前 HTTP handler 等待 efka command response 的超时时间固定为 10 秒。
|
||
|
||
## 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[] | `[]` | 容器暴露端口,只表示容器端口,不支持宿主机端口绑定。 |
|
||
| `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` | integer | `0` | Docker CPU shares。 |
|
||
| `cpus` | 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` | 健康检查配置,格式见后文。 |
|
||
|
||
### 不支持字段
|
||
|
||
| 字段 | 当前行为 | 说明 |
|
||
| --- | --- | --- |
|
||
| `ports` | 明确拒绝 | 如果传入会返回 `unsupported container config keys: ports`。当前只支持 `expose`,不支持宿主机端口绑定。 |
|
||
| `container_dir` | 明确拒绝 | 容器目录由 efka 按系统默认规则管理,HTTP 调用方不能指定 efka 主机上的部署目录。 |
|
||
| `env_file` | 忽略 | 当前校验不会识别该字段,后续构造 Docker create options 时也不会使用。 |
|
||
| 其他未知字段 | 忽略 | 除 `ports` 和 `container_dir` 外,未知字段不会报错,也不会进入内部部署参数。 |
|
||
|
||
## 5. 校验规则
|
||
|
||
校验分为两层。
|
||
|
||
第一层在 `container_handler`:
|
||
|
||
- `uuid` 必须是 binary。
|
||
- `task_id` 必须是 integer。
|
||
- `config` 必须是 map。
|
||
|
||
第二层在 `docker_container_builder:deploy_request/2`:
|
||
|
||
- 先拒绝不支持的 `ports` 和 `container_dir` 字段。
|
||
- 检查必填字段是否存在。
|
||
- 检查已知字段类型。
|
||
- 构造内部部署 map。
|
||
- 对需要解析的字段做格式解析,例如端口、volume、size、duration、ulimit。
|
||
|
||
类型错误会返回类似:
|
||
|
||
```text
|
||
required parameter: <<"command">>, type must be: list of string
|
||
optional parameter: <<"labels">>, type must be: map of string:string
|
||
```
|
||
|
||
缺少必填字段会返回类似:
|
||
|
||
```text
|
||
miss requied parameter: <<"image">>
|
||
```
|
||
|
||
注意:错误文本中的 `requied` 是当前代码里的原始拼写。
|
||
|
||
## 6. 内部转换结果
|
||
|
||
`config` 校验通过后,`docker_container_builder:deploy_request(TaskId, Config)` 生成:
|
||
|
||
```erlang
|
||
#{
|
||
action => deploy,
|
||
task_id => TaskId,
|
||
params => #{
|
||
container_name => ContainerName,
|
||
create => #{
|
||
config => ContainerConfig,
|
||
host_config => HostConfig,
|
||
networking_config => NetworkingConfig
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
该 map 会通过 efka/iot 长连接协议下发:
|
||
|
||
```erlang
|
||
{command, Ref, {container, #{
|
||
action => deploy,
|
||
task_id => TaskId,
|
||
params => Params
|
||
}}}
|
||
```
|
||
|
||
efka 返回:
|
||
|
||
```erlang
|
||
{command_response, Ref, {container, Reply}}
|
||
```
|
||
|
||
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` | 转成 `#{container_port, protocol}` map 列表。 |
|
||
| `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`。 |
|
||
| `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` 生成:
|
||
|
||
```erlang
|
||
#{endpoints => [#{name => Network} || Network <- Networks]}
|
||
```
|
||
|
||
来源字段:
|
||
|
||
```json
|
||
"networks": ["bridge", "mynet"]
|
||
```
|
||
|
||
转换结果:
|
||
|
||
```erlang
|
||
#{endpoints => [
|
||
#{name => <<"bridge">>},
|
||
#{name => <<"mynet">>}
|
||
]}
|
||
```
|
||
|
||
## 10. 复杂字段转换规则
|
||
|
||
### restart
|
||
|
||
输入:
|
||
|
||
```json
|
||
"restart": "always"
|
||
```
|
||
|
||
转换:
|
||
|
||
```erlang
|
||
#{name => <<"always">>, maximum_retry_count => 0}
|
||
```
|
||
|
||
输入:
|
||
|
||
```json
|
||
"restart": "on-failure:3"
|
||
```
|
||
|
||
转换:
|
||
|
||
```erlang
|
||
#{name => <<"on-failure">>, maximum_retry_count => 3}
|
||
```
|
||
|
||
### expose
|
||
|
||
输入:
|
||
|
||
```json
|
||
["80", "443/tcp", "53/udp"]
|
||
```
|
||
|
||
转换:
|
||
|
||
```erlang
|
||
[
|
||
#{container_port => 80, protocol => <<"tcp">>},
|
||
#{container_port => 443, protocol => <<"tcp">>},
|
||
#{container_port => 53, protocol => <<"udp">>}
|
||
]
|
||
```
|
||
|
||
端口必须是无符号整数,且不能超过 `4294967295`。
|
||
|
||
### volumes
|
||
|
||
输入:
|
||
|
||
```json
|
||
["/host/data:/data", "/host/log:/var/log:ro", "/host/cache:/cache:rw"]
|
||
```
|
||
|
||
转换为 `create.config.volumes`:
|
||
|
||
```erlang
|
||
[<<"/data">>, <<"/var/log">>, <<"/cache">>]
|
||
```
|
||
|
||
转换为 `create.host_config.binds`:
|
||
|
||
```erlang
|
||
[
|
||
<<"/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
|
||
|
||
输入:
|
||
|
||
```json
|
||
["/dev/ttyUSB0:/dev/ttyUSB0", "/dev/snd:/dev/snd:rwm"]
|
||
```
|
||
|
||
转换:
|
||
|
||
```erlang
|
||
[
|
||
#{
|
||
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
|
||
|
||
输入:
|
||
|
||
```json
|
||
{
|
||
"nofile": "1024:2048",
|
||
"nproc": "4096"
|
||
}
|
||
```
|
||
|
||
转换:
|
||
|
||
```erlang
|
||
[
|
||
#{name => <<"nofile">>, soft => 1024, hard => 2048},
|
||
#{name => <<"nproc">>, soft => 4096, hard => 4096}
|
||
]
|
||
```
|
||
|
||
规则:
|
||
|
||
- `"soft:hard"` -> 分别设置 soft 和 hard。
|
||
- `"limit"` -> soft 和 hard 都等于 limit。
|
||
- 数值必须是无符号整数。
|
||
|
||
### tmpfs
|
||
|
||
输入:
|
||
|
||
```json
|
||
["/tmp", "/run:rw,size=64m"]
|
||
```
|
||
|
||
转换:
|
||
|
||
```erlang
|
||
#{
|
||
<<"/tmp">> => <<>>,
|
||
<<"/run">> => <<"rw,size=64m">>
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- `"path"` -> options 为 `""`。
|
||
- `"path:options"` -> options 为第二段。
|
||
- path 不能为空。
|
||
|
||
### healthcheck
|
||
|
||
输入:
|
||
|
||
```json
|
||
{
|
||
"test": ["CMD-SHELL", "curl -f http://localhost || exit 1"],
|
||
"interval": "30s",
|
||
"timeout": "10s",
|
||
"retries": 3
|
||
}
|
||
```
|
||
|
||
转换:
|
||
|
||
```erlang
|
||
#{
|
||
test => [<<"CMD-SHELL">>, <<"curl -f http://localhost || exit 1">>],
|
||
interval_ns => 30000000000,
|
||
timeout_ns => 10000000000,
|
||
retries => 3
|
||
}
|
||
```
|
||
|
||
字段规则:
|
||
|
||
| 字段 | JSON 类型 | 默认值 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `test` | any | `[]` | 当前只要求 `healthcheck` 的 key 是 string,不单独校验 `test` 类型;建议传 string array。 |
|
||
| `interval` | string 或 integer | `"0s"` | string 会解析时间单位;integer 直接视为纳秒。 |
|
||
| `timeout` | string 或 integer | `"0s"` | string 会解析时间单位;integer 直接视为纳秒。 |
|
||
| `retries` | integer | `0` | 当前构造阶段直接取值;建议传非负整数。 |
|
||
|
||
时间单位:
|
||
|
||
| 单位 | 含义 |
|
||
| --- | --- |
|
||
| `ns` | 纳秒 |
|
||
| `us` | 微秒 |
|
||
| `ms` | 毫秒 |
|
||
| `s` | 秒 |
|
||
| `m` | 分钟 |
|
||
| `h` | 小时 |
|
||
| 无单位 | 秒 |
|
||
|
||
示例:
|
||
|
||
- `"30s"` -> `30000000000`
|
||
- `"10ms"` -> `10000000`
|
||
- `"2m"` -> `120000000000`
|
||
- `"5"` -> `5000000000`
|
||
|
||
### mem_limit 和 mem_reservation
|
||
|
||
输入:
|
||
|
||
```json
|
||
"mem_limit": "512m",
|
||
"mem_reservation": "1g"
|
||
```
|
||
|
||
转换:
|
||
|
||
```erlang
|
||
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
|
||
|
||
输入:
|
||
|
||
```json
|
||
"cpus": 1.5
|
||
```
|
||
|
||
转换:
|
||
|
||
```erlang
|
||
nano_cpus => 1500000000
|
||
```
|
||
|
||
规则:
|
||
|
||
- integer 或 float 都可以。
|
||
- 必须大于等于 0。
|
||
- 转换公式:`trunc(Cpus * 1000000000)`。
|
||
|
||
## 11. 最小请求示例
|
||
|
||
```json
|
||
{
|
||
"uuid": "qbxmjyzrkpntfgswaevodhluicqzxplkm",
|
||
"task_id": 1001,
|
||
"config": {
|
||
"image": "docker.io/library/nginx:latest",
|
||
"container_name": "my_nginx",
|
||
"command": ["nginx", "-g", "daemon off;"],
|
||
"restart": "always"
|
||
}
|
||
}
|
||
```
|
||
|
||
对应内部命令示意:
|
||
|
||
```erlang
|
||
{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,
|
||
ulimits => [],
|
||
tmpfs => #{},
|
||
sysctls => #{},
|
||
extra_hosts => []
|
||
},
|
||
networking_config => #{endpoints => []}
|
||
}
|
||
}
|
||
}}}
|
||
```
|
||
|
||
## 12. 响应
|
||
|
||
成功时 HTTP body 由 `iot_util:json_data/1` 包装:
|
||
|
||
```json
|
||
{
|
||
"result": "ok"
|
||
}
|
||
```
|
||
|
||
如果 efka 返回的是 JSON binary,handler 会尝试解码后再放入 `result`。
|
||
|
||
错误示例:
|
||
|
||
```json
|
||
{
|
||
"error": {
|
||
"code": 400,
|
||
"message": "unsupported container config keys: ports"
|
||
}
|
||
}
|
||
```
|
||
|
||
常见状态:
|
||
|
||
| HTTP 状态 | 场景 |
|
||
| --- | --- |
|
||
| `200` | host not found 或构造阶段返回的业务错误也可能通过 200 包装业务错误。 |
|
||
| `400` | 参数校验失败、efka 返回普通错误。 |
|
||
| `504` | 等待 efka command response 超时。 |
|