ekfa/docs/container_command_docker_json.md

738 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# IOT 容器命令到 Docker JSON 的转换说明
本文档描述 `efka` 收到 `iot` 下发的容器管理 command 后,接受的 Erlang map 格式,以及 deploy 命令中 `create` 参数如何转换成 Docker Engine API 接收的 JSON。
对应代码:
- command 接收入口:[src/transport/efka_client.erl](/usr/local/code/cloudkit/efka/src/transport/efka_client.erl:177)
- 部署任务管理:[src/docker/docker_deploy_manager.erl](/usr/local/code/cloudkit/efka/src/docker/docker_deploy_manager.erl:36)
- 部署执行:[src/docker/docker_deployer.erl](/usr/local/code/cloudkit/efka/src/docker/docker_deployer.erl:39)
- Docker JSON 构造:[src/docker/docker_container_builder.erl](/usr/local/code/cloudkit/efka/src/docker/docker_container_builder.erl:14)
- Docker API 调用:[src/docker/docker_commands.erl](/usr/local/code/cloudkit/efka/src/docker/docker_commands.erl:36)
## 1. 协议入口
`iot` 通过 TLS 长连接向 `efka` 下发容器命令:
```erlang
{command, Ref, {container, CommandMap}}
```
`efka` 执行后回复:
```erlang
{command_response, Ref, {container, Reply}}
```
`Reply` 取值:
```erlang
ok
{ok, Result}
{error, Reason}
```
只有 `efka_client` 处于 `activated` 状态时,容器命令才会正常执行;处于 `restricted` 或其他状态时会返回错误。
## 2. 容器命令 map
### list
```erlang
#{action => list}
```
执行:
```erlang
docker_commands:get_containers()
```
Docker API
```http
GET /containers/json?all=true
```
### deploy
```erlang
#{
action => deploy,
task_id => TaskId,
params => Params
}
```
执行:
```erlang
docker_deploy_manager:deploy(TaskId, Params)
```
`deploy/2` 会启动独立部署进程HTTP command response 只表示部署任务是否成功启动。实际部署过程和结果通过 `task_event` 消息流上报给 `iot`
### start
```erlang
#{
action => start,
target => Target
}
```
执行:
```erlang
docker_commands:start_container(ContainerNameOrId)
```
Docker API
```http
POST /containers/{name_or_id}/start
```
### stop
```erlang
#{
action => stop,
target => Target,
timeout_seconds => TimeoutSeconds
}
```
执行:
```erlang
docker_commands:stop_container(ContainerNameOrId, TimeoutSeconds)
```
Docker API
```http
POST /containers/{name_or_id}/stop?t={TimeoutSeconds}
```
### kill
```erlang
#{
action => kill,
target => Target,
signal => Signal
}
```
执行:
```erlang
docker_commands:kill_container(ContainerNameOrId, Signal)
```
### remove
```erlang
#{
action => remove,
target => Target,
force => Force,
remove_volumes => RemoveVolumes
}
```
执行:
```erlang
docker_commands:remove_container(ContainerNameOrId, Force, RemoveVolumes)
```
### config
```erlang
#{
action => config,
target => Target,
config => Config
}
```
执行:
```erlang
docker_helper:update_container_config(ContainerNameOrId, iolist_to_binary(Config))
```
该命令不会调用 Docker API只会更新 `efka` 主机上对应容器目录里的 `service.conf`
## 3. Target 解析规则
`target` 是 map
```erlang
#{
name => ContainerName,
id => ContainerId
}
```
解析规则:
- 优先使用 `name`
- `name` 为空时使用 `id`
- `name``id` 都为空时会触发匹配错误。
## 4. deploy params 格式
`deploy``params` 必须包含:
```erlang
#{
container_name => ContainerName,
container_dir => ContainerDir,
create => Create
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `container_name` | binary | 容器名称。用于容器创建 URL 的 `name` 参数,也会注入环境变量 `CONTAINER_NAME`。 |
| `container_dir` | binary | 容器在 efka 主机上的应用目录。为空时使用 `efka.root_dir/ContainerName/`。 |
| `create` | map | Docker create options 的中间结构,由 `iot` 侧构造,`efka` 侧补丁后转为 Docker JSON。 |
`docker_deploy_manager` 会根据 `container_name``container_dir` 确保容器目录存在,并写入 `.container_dir` 指针文件。
## 5. create 中间结构
`create` 的结构:
```erlang
#{
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` 前置注入:
```erlang
<<"CONTAINER_NAME=", ContainerName/binary>>
```
如果原 env 列表已经包含完全相同的值,则不会重复添加。
### 配置文件 volume
`create.config.volumes` 前置注入容器内配置路径:
```erlang
<<"/usr/local/etc/service.conf">>
```
### 配置文件 bind
`create.host_config.binds` 前置注入宿主机配置文件映射:
```erlang
<<ConfigFile/binary, ":/usr/local/etc/service.conf">>
```
其中 `ConfigFile` 是当前容器目录下的:
```text
service.conf
```
如果列表里已存在完全相同的 bind则不会重复添加。
## 7. Docker JSON 顶层结构
`docker_commands:create_container/2` 最终调用:
```http
POST /containers/create?name={container_name}
Content-Type: application/json
```
请求 body 来自:
```erlang
Options = docker_container_builder:build_options(ContainerName, ContainerDir, Create),
Body = iolist_to_binary(json:encode(Options))
```
最终 JSON 顶层字段:
```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 要求的 objectkey 是容器内路径value 是 `{}`。 |
| `user` | `User` | 转成 binary缺省为 `""`。 |
| `working_dir` | `WorkingDir` | 转成 binary缺省为 `""`。 |
| `hostname` | `Hostname` | 转成 binary缺省为 `""`。 |
| `exposed_ports` | `ExposedPorts` | 转成 Docker 端口 object。 |
| `healthcheck` | `Healthcheck` | 转成 Docker Healthcheck object未传时为 `{}`。 |
### Volumes
输入:
```erlang
[<<"/data">>, <<"/usr/local/etc/service.conf">>]
```
输出 JSON
```json
{
"Volumes": {
"/data": {},
"/usr/local/etc/service.conf": {}
}
}
```
### ExposedPorts
输入:
```erlang
[
#{container_port => 80, protocol => <<"tcp">>},
#{container_port => 53, protocol => <<"udp">>}
]
```
输出 JSON
```json
{
"ExposedPorts": {
"80/tcp": {},
"53/udp": {}
}
}
```
规则:
- `protocol` 为空或 `<<"tcp">>` 时输出 `tcp`
- 其他 protocol 原样输出。
### Healthcheck
输入:
```erlang
#{
test => [<<"CMD-SHELL">>, <<"curl -f http://localhost || exit 1">>],
interval_ns => 30000000000,
timeout_ns => 10000000000,
retries => 3
}
```
输出 JSON
```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
输入:
```erlang
[
<<"/host/data:/data">>,
<<"/host/log:/var/log:ro">>
]
```
efka 自动补丁后输出:
```json
{
"HostConfig": {
"Binds": [
"/path/to/container/service.conf:/usr/local/etc/service.conf",
"/host/data:/data",
"/host/log:/var/log:ro"
]
}
}
```
### RestartPolicy
输入:
```erlang
#{name => <<"always">>, maximum_retry_count => 0}
```
输出:
```json
{
"HostConfig": {
"RestartPolicy": {
"Name": "always"
}
}
}
```
输入:
```erlang
#{name => <<"on-failure">>, maximum_retry_count => 3}
```
输出:
```json
{
"HostConfig": {
"RestartPolicy": {
"Name": "on-failure",
"MaximumRetryCount": 3
}
}
}
```
### Devices
输入:
```erlang
[
#{
path_on_host => <<"/dev/ttyUSB0">>,
path_in_container => <<"/dev/ttyUSB0">>,
cgroup_permissions => <<"rwm">>
}
]
```
输出:
```json
{
"HostConfig": {
"Devices": [
{
"PathOnHost": "/dev/ttyUSB0",
"PathInContainer": "/dev/ttyUSB0",
"CgroupPermissions": "rwm"
}
]
}
}
```
`cgroup_permissions` 为空时默认输出 `rwm`
### 资源限制
输入:
```erlang
#{
memory => 536870912,
memory_reservation => 268435456,
nano_cpus => 1500000000,
cpu_shares => 512
}
```
输出:
```json
{
"HostConfig": {
"Memory": 536870912,
"MemoryReservation": 268435456,
"NanoCpus": 1500000000,
"CpuShares": 512
}
}
```
值为 0 的资源字段会被省略。
### Ulimits
输入:
```erlang
[
#{name => <<"nofile">>, soft => 1024, hard => 2048}
]
```
输出:
```json
{
"HostConfig": {
"Ulimits": [
{
"Name": "nofile",
"Soft": 1024,
"Hard": 2048
}
]
}
}
```
### Tmpfs
输入:
```erlang
#{
<<"/tmp">> => <<>>,
<<"/run">> => <<"rw,size=64m">>
}
```
输出:
```json
{
"HostConfig": {
"Tmpfs": {
"/tmp": "",
"/run": "rw,size=64m"
}
}
}
```
### Sysctls
输入:
```erlang
#{<<"net.ipv4.ip_forward">> => <<"1">>}
```
输出:
```json
{
"HostConfig": {
"Sysctls": {
"net.ipv4.ip_forward": "1"
}
}
}
```
### ExtraHosts
输入:
```erlang
[<<"host.docker.internal:host-gateway">>]
```
输出:
```json
{
"HostConfig": {
"ExtraHosts": ["host.docker.internal:host-gateway"]
}
}
```
## 10. create.networking_config 到 Docker JSON
输入:
```erlang
#{
endpoints => [
#{name => <<"bridge">>},
#{name => <<"mynet">>}
]
}
```
输出:
```json
{
"NetworkingConfig": {
"EndpointsConfig": {
"bridge": {},
"mynet": {}
}
}
}
```
未传或 endpoints 为空时输出:
```json
{
"NetworkingConfig": {}
}
```
## 11. 完整转换示例
收到的 deploy command
```erlang
{command, Ref, {container, #{
action => deploy,
task_id => 1001,
params => #{
container_name => <<"my_nginx">>,
container_dir => <<"/data/apps/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 示意:
```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": [
"/data/apps/my_nginx/service.conf:/usr/local/etc/service.conf",
"/host/data:/data"
],
"RestartPolicy": {
"Name": "always"
},
"Memory": 536870912
}
}
```
## 12. 部署流程补充
deploy action 的执行流程:
1. 根据 `container_name``container_dir` 确保容器目录存在。
2. 启动独立部署进程。
3. 部署进程上报任务事件流。
4. 规范化镜像名:如果镜像最后一段没有 tag则补 `:latest`
5. 调用 Docker API 拉取镜像。
6. 构造 Docker create JSON。
7. 调用 `POST /containers/create?name={container_name}`
8. 创建空 `service.conf` 文件。
9. 写入部署摘要日志并关闭任务事件流。
注意:当前 `ensure_container_absent/2` 只上报“开始创建容器”,不会删除已有同名容器;如果 Docker 返回名称冲突,部署会失败并上报“本地容器已经存在”。