iot_cloud/docs/container_deploy_request.md
2026-04-19 15:09:31 +08:00

526 lines
11 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.

# Container Deploy 请求格式说明
本文档说明 HTTP 接口 `/container/deploy` 接收的 JSON 格式,以及它在服务端如何被转换为 `message.proto` 中的 `ContainerRequest.Deploy` / `ContainerDeployParams` / `ContainerSpec`
当前实现对应代码:
- HTTP 入口:[src/transport/http/container_handler.erl](/usr/local/code/cloudkit/iot/src/transport/http/container_handler.erl:63)
- 请求构造器与 deploy 配置校验:[src/host/iot_container_request_builder.erl](/usr/local/code/cloudkit/iot/src/host/iot_container_request_builder.erl:25)
- protobuf 定义:[proto/message.proto](/usr/local/code/cloudkit/iot/proto/message.proto:37)
## 1. HTTP 请求格式
接口:
- `POST /container/deploy`
请求体顶层结构:
```json
{
"uuid": "host-uuid",
"task_id": 1001,
"config": {
"image": "docker.io/library/nginx:latest",
"container_name": "my_nginx",
"container_dir": "/data/apps/my_nginx",
"command": ["nginx", "-g", "daemon off;"],
"entrypoint": ["/docker-entrypoint.sh"],
"envs": ["ENV1=val1", "ENV2=val2"],
"expose": ["80", "443/tcp"],
"volumes": ["/host/data:/data", "/host/log:/var/log:ro"],
"networks": ["bridge"],
"network_mode": "bridge",
"labels": {
"role": "web",
"env": "prod"
},
"restart": "always",
"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
}
}
}
```
## 2. 顶层字段与 protobuf 的关系
HTTP 顶层字段和 protobuf 请求体的关系如下:
| HTTP 字段 | 类型 | 目标 protobuf 字段 | 说明 |
| --- | --- | --- | --- |
| `uuid` | `string` | 不进入 protobuf body | 仅用于在服务端定位 `iot_host` 进程 |
| `task_id` | `integer` | `ContainerRequest.Deploy.task_id` | 必填,用于异步消息集合标识 |
| `config` | `object` | `ContainerRequest.Deploy.params` | 必填,转换成 `ContainerDeployParams` |
最终构造出来的请求体结构是:
```text
ContainerRequest {
action = {
deploy,
ContainerRequest.Deploy {
task_id = TaskId,
params = ContainerDeployParams {...}
}
}
}
```
## 3. `config` 到 `ContainerDeployParams` 的映射
`config` 会被转换成:
```text
ContainerDeployParams {
container_name,
container_dir,
spec = ContainerSpec {...}
}
```
字段映射如下:
| `config` 字段 | 类型 | protobuf 字段 | 说明 |
| --- | --- | --- | --- |
| `container_name` | `string` | `ContainerDeployParams.container_name` | 必填 |
| `container_dir` | `string` | `ContainerDeployParams.container_dir` | 可选,默认 `""` |
| 其余部署字段 | 多种 | `ContainerDeployParams.spec` | 转成 `ContainerSpec` |
## 4. `config` 到 `ContainerSpec` 的详细映射
### 4.1 直接映射字段
以下字段基本按原值写入:
| `config` 字段 | 类型 | protobuf 字段 | 默认值 |
| --- | --- | --- | --- |
| `image` | `string` | `ContainerSpec.image` | 必填 |
| `command` | `string[]` | `ContainerSpec.command` | 必填 |
| `entrypoint` | `string[]` | `ContainerSpec.entrypoint` | `[]` |
| `envs` | `string[]` | `ContainerSpec.env` | `[]` |
| `networks` | `string[]` | `ContainerSpec.networks` | `[]` |
| `network_mode` | `string` | `ContainerSpec.network_mode` | `""` |
| `user` | `string` | `ContainerSpec.user` | `""` |
| `working_dir` | `string` | `ContainerSpec.working_dir` | `""` |
| `hostname` | `string` | `ContainerSpec.hostname` | `""` |
| `privileged` | `boolean` | `ContainerSpec.privileged` | `false` |
| `cap_add` | `string[]` | `ContainerSpec.cap_add` | `[]` |
| `cap_drop` | `string[]` | `ContainerSpec.cap_drop` | `[]` |
| `extra_hosts` | `string[]` | `ContainerSpec.extra_hosts` | `[]` |
### 4.2 map 转换字段
| `config` 字段 | 类型 | protobuf 字段 | 转换方式 |
| --- | --- | --- | --- |
| `labels` | `map<string,string>` | `ContainerSpec.labels` | 转成 key/value 列表 |
| `sysctls` | `map<string,string>` | `ContainerSpec.sysctls` | 转成 key/value 列表 |
### 4.3 结构化转换字段
#### `volumes`
输入类型:
```json
["/host/data:/data", "/host/log:/var/log:ro"]
```
目标:
```text
ContainerSpec.volumes = [VolumeBind...]
```
转换规则:
- `host_path:container_path`
- `read_only = false`
- `host_path:container_path:ro`
- `read_only = true`
- `host_path:container_path:rw`
- 当前实现也会被接受,但 `read_only = false`
生成结构:
```text
VolumeBind {
host_path,
container_path,
read_only
}
```
#### `expose`
输入类型:
```json
["80", "443/tcp", "53/udp"]
```
目标:
```text
ContainerSpec.expose = [PortExpose...]
```
转换规则:
- `"80"` -> `container_port = 80`, `protocol = "tcp"`
- `"443/tcp"` -> `container_port = 443`, `protocol = "tcp"`
- `"53/udp"` -> `container_port = 53`, `protocol = "udp"`
生成结构:
```text
PortExpose {
container_port,
protocol
}
```
#### `devices`
输入类型:
```json
["/dev/ttyUSB0:/dev/ttyUSB0", "/dev/snd:/dev/snd:rwm"]
```
目标:
```text
ContainerSpec.devices = [DeviceMapping...]
```
转换规则:
- `host_path:container_path`
- `cgroup_permissions = "rwm"`
- `host_path:container_path:permissions`
- 使用第三段作为 `cgroup_permissions`
#### `ulimits`
输入类型:
```json
{
"nofile": "1024:2048",
"nproc": "4096"
}
```
目标:
```text
ContainerSpec.ulimits = [Ulimit...]
```
转换规则:
- `"1024:2048"` -> `soft = 1024`, `hard = 2048`
- `"4096"` -> `soft = 4096`, `hard = 4096`
#### `tmpfs`
输入类型:
```json
["/tmp", "/run:rw,size=64m"]
```
目标:
```text
ContainerSpec.tmpfs = [TmpfsMount...]
```
转换规则:
- `"path"` -> `options = ""`
- `"path:options"` -> `options` 为第二段
#### `healthcheck`
输入类型:
```json
{
"test": ["CMD-SHELL", "curl -f http://localhost || exit 1"],
"interval": "30s",
"timeout": "10s",
"retries": 3
}
```
目标:
```text
ContainerSpec.healthcheck = Healthcheck {
test,
interval_ns,
timeout_ns,
retries
}
```
转换规则:
- `test` 直接写入 `Healthcheck.test`
- `interval` / `timeout` 会被解析成纳秒
- `retries` 直接写入
支持的时间单位:
- `ns`
- `us`
- `ms`
- `s`
- `m`
- `h`
示例:
- `"30s"` -> `30000000000`
- `"10ms"` -> `10000000`
- `"2m"` -> `120000000000`
### 4.4 资源限制字段
#### `restart`
输入类型:
```json
"always"
```
或:
```json
"on-failure:3"
```
目标:
```text
RestartPolicy {
name,
maximum_retry_count
}
```
转换规则:
- `"always"` -> `name = "always"`, `maximum_retry_count = 0`
- `"on-failure:3"` -> `name = "on-failure"`, `maximum_retry_count = 3`
#### `mem_limit` / `mem_reservation`
目标字段:
- `ResourceLimits.memory_bytes`
- `ResourceLimits.memory_reservation_bytes`
支持单位:
- `b`
- `k`, `kb`, `ki`, `kib`
- `m`, `mb`, `mi`, `mib`
- `g`, `gb`, `gi`, `gib`
- `t`, `tb`, `ti`, `tib`
示例:
- `"512m"` -> `536870912`
- `"1g"` -> `1073741824`
#### `cpus`
目标字段:
- `ResourceLimits.nano_cpus`
转换规则:
- 直接乘以 `1_000_000_000`
示例:
- `1` -> `1000000000`
- `1.5` -> `1500000000`
#### `cpu_shares`
目标字段:
- `ResourceLimits.cpu_shares`
直接按整数写入。
#### `resources` 对象生成规则
只有在以下字段至少存在一个时,才会生成 `ContainerSpec.resources`
- `mem_limit`
- `mem_reservation`
- `cpus`
- `cpu_shares`
如果这些字段都不存在,则 `resources = undefined`
## 5. 参数校验规则
当前实现里:
- `container_handler` 只校验顶层请求结构:
- `uuid` 必须是 `binary`
- `task_id` 必须是 `integer`
- `config` 必须是 `map`
- `config` 内部字段的必填项、类型检查、格式解析和不支持字段判断,全部在 `iot_container_request_builder:deploy_request/2` 中完成
必填字段:
- `image: string`
- `container_name: string`
- `command: string[]`
- `restart: string`
可选字段:
- `privileged: boolean`
- `entrypoint: string[]`
- `envs: string[]`
- `ports: string[]`
- `expose: string[]`
- `volumes: string[]`
- `networks: string[]`
- `labels: map<string,string>`
- `user: string`
- `working_dir: string`
- `hostname: string`
- `container_dir: string`
- `network_mode: string`
- `cap_add: string[]`
- `cap_drop: string[]`
- `devices: string[]`
- `mem_limit: string`
- `mem_reservation: string`
- `cpu_shares: integer`
- `cpus: number`
- `ulimits: map<string,string>`
- `sysctls: map<string,string>`
- `tmpfs: string[]`
- `extra_hosts: string[]`
- `healthcheck: map<string,any>`
## 6. 当前限制
### `ports`
虽然 HTTP 校验允许 `ports` 字段出现,但当前 builder 仍然会拒绝它:
```text
unsupported container config keys: ports
```
原因是当前 `message.proto` 里只有:
- `ContainerSpec.expose`
它表达的是容器端口暴露,不包含主机端口绑定信息;而 `ports` 一般是类似 `8080:80` 的 host/container 绑定语义,两者并不等价。
### `env_file`
当前实现不再接收 `env_file`。如果传入HTTP 层就不会通过类型校验,因为它不在 `validate_config/1` 的允许字段列表中。
## 7. 推荐请求示例
```json
{
"uuid": "qbxmjyzrkpntfgswaevodhluicqzxplkm",
"task_id": 1001,
"config": {
"image": "docker.io/library/nginx:latest",
"container_name": "my_nginx",
"container_dir": "/data/apps/my_nginx",
"command": ["nginx", "-g", "daemon off;"],
"entrypoint": ["/docker-entrypoint.sh"],
"envs": ["ENV=prod", "TZ=Asia/Shanghai"],
"expose": ["80", "443/tcp"],
"volumes": ["/host/data:/data", "/host/log:/var/log:ro"],
"networks": ["bridge"],
"network_mode": "bridge",
"labels": {
"app": "nginx",
"env": "prod"
},
"restart": "always",
"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
}
}
}
```
## 8. 后续如果要支持 `ports`
建议先扩展 `message.proto`,增加明确表达 host/container 端口绑定的结构,例如:
```proto
message PortBinding {
uint32 host_port = 1;
uint32 container_port = 2;
string protocol = 3;
string host_ip = 4;
}
```
然后再在 `ContainerSpec` 中增加类似 `repeated PortBinding ports = ...;` 的字段,再由 builder 把 `"8080:80/tcp"` 这类字符串解析进去。这样语义才完整。