ekfa/docs/container_command_docker_json.md

14 KiB
Raw Blame History

IOT 容器命令到 Docker JSON 的转换说明

本文档描述 efka 收到 iot 下发的容器管理 command 后,接受的 Erlang map 格式,以及 deploy 命令中 create 参数如何转换成 Docker Engine API 接收的 JSON。

对应代码:

1. 协议入口

iot 通过 TLS 长连接向 efka 下发容器命令:

{command, Ref, {container, CommandMap}}

efka 执行后回复:

{command_response, Ref, {container, Reply}}

Reply 取值:

ok
{ok, Result}
{error, Reason}

只有 efka_client 处于 activated 状态时,容器命令才会正常执行;处于 restricted 或其他状态时会返回错误。

2. 容器命令 map

list

#{action => list}

执行:

docker_commands:get_containers()

Docker API

GET /containers/json?all=true

deploy

#{
    action => deploy,
    task_id => TaskId,
    params => Params
}

执行:

docker_deploy_manager:deploy(TaskId, Params)

deploy/2 会启动独立部署进程HTTP command response 只表示部署任务是否成功启动。实际部署过程和结果通过 task_event 消息流上报给 iot

start

#{
    action => start,
    target => Target
}

执行:

docker_commands:start_container(ContainerNameOrId)

Docker API

POST /containers/{name_or_id}/start

stop

#{
    action => stop,
    target => Target,
    timeout_seconds => TimeoutSeconds
}

执行:

docker_commands:stop_container(ContainerNameOrId, TimeoutSeconds)

Docker API

POST /containers/{name_or_id}/stop?t={TimeoutSeconds}

kill

#{
    action => kill,
    target => Target,
    signal => Signal
}

执行:

docker_commands:kill_container(ContainerNameOrId, Signal)

remove

#{
    action => remove,
    target => Target,
    force => Force,
    remove_volumes => RemoveVolumes
}

执行:

docker_commands:remove_container(ContainerNameOrId, Force, RemoveVolumes)

config

#{
    action => config,
    target => Target,
    config => Config
}

执行:

docker_helper:update_container_config(ContainerNameOrId, iolist_to_binary(Config))

该命令不会调用 Docker API只会更新 efka 主机上对应容器目录里的 service.conf

3. Target 解析规则

target 是 map

#{
    name => ContainerName,
    id => ContainerId
}

解析规则:

  • 优先使用 name
  • name 为空时使用 id
  • nameid 都为空时会触发匹配错误。

4. deploy params 格式

deployparams 必须包含:

#{
    container_name => ContainerName,
    create => Create
}

字段说明:

字段 类型 说明
container_name binary 容器名称。用于容器创建 URL 的 name 参数,也会注入环境变量 CONTAINER_NAME
create map Docker create options 的中间结构,由 iot 侧构造,efka 侧补丁后转为 Docker JSON。

docker_deploy_manager 会根据 container_name 按系统默认规则确保容器目录存在。目录固定为:

efka.root_dir/container_name/

即使旧客户端在 params 中携带 container_dir,当前 deploy 流程也会忽略它,避免 HTTP 调用方控制 efka 主机上的写入目录。

5. create 中间结构

create 的结构:

#{
    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 前置注入:

<<"CONTAINER_NAME=", ContainerName/binary>>

如果原 env 列表已经包含完全相同的值,则不会重复添加。

配置文件 volume

create.config.volumes 前置注入容器内配置路径:

<<"/usr/local/etc/service.conf">>

配置文件 bind

create.host_config.binds 前置注入宿主机配置文件映射:

<<ConfigFile/binary, ":/usr/local/etc/service.conf">>

其中 ConfigFile 是当前容器目录下的:

service.conf

如果列表里已存在完全相同的 bind则不会重复添加。

7. Docker JSON 顶层结构

docker_commands:create_container/2 最终调用:

POST /containers/create?name={container_name}
Content-Type: application/json

请求 body 来自:

Options = docker_container_builder:build_options(ContainerName, ContainerDir, Create),
Body = iolist_to_binary(json:encode(Options))

最终 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

输入:

[<<"/data">>, <<"/usr/local/etc/service.conf">>]

输出 JSON

{
  "Volumes": {
    "/data": {},
    "/usr/local/etc/service.conf": {}
  }
}

ExposedPorts

输入:

[
    #{container_port => 80, protocol => <<"tcp">>},
    #{container_port => 53, protocol => <<"udp">>}
]

输出 JSON

{
  "ExposedPorts": {
    "80/tcp": {},
    "53/udp": {}
  }
}

规则:

  • protocol 为空或 <<"tcp">> 时输出 tcp
  • 其他 protocol 原样输出。

Healthcheck

输入:

#{
    test => [<<"CMD-SHELL">>, <<"curl -f http://localhost || exit 1">>],
    interval_ns => 30000000000,
    timeout_ns => 10000000000,
    retries => 3
}

输出 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

输入:

[
    <<"/host/data:/data">>,
    <<"/host/log:/var/log:ro">>
]

efka 自动补丁后输出:

{
  "HostConfig": {
    "Binds": [
      "/path/to/container/service.conf:/usr/local/etc/service.conf",
      "/host/data:/data",
      "/host/log:/var/log:ro"
    ]
  }
}

RestartPolicy

输入:

#{name => <<"always">>, maximum_retry_count => 0}

输出:

{
  "HostConfig": {
    "RestartPolicy": {
      "Name": "always"
    }
  }
}

输入:

#{name => <<"on-failure">>, maximum_retry_count => 3}

输出:

{
  "HostConfig": {
    "RestartPolicy": {
      "Name": "on-failure",
      "MaximumRetryCount": 3
    }
  }
}

Devices

输入:

[
    #{
        path_on_host => <<"/dev/ttyUSB0">>,
        path_in_container => <<"/dev/ttyUSB0">>,
        cgroup_permissions => <<"rwm">>
    }
]

输出:

{
  "HostConfig": {
    "Devices": [
      {
        "PathOnHost": "/dev/ttyUSB0",
        "PathInContainer": "/dev/ttyUSB0",
        "CgroupPermissions": "rwm"
      }
    ]
  }
}

cgroup_permissions 为空时默认输出 rwm

资源限制

输入:

#{
    memory => 536870912,
    memory_reservation => 268435456,
    nano_cpus => 1500000000,
    cpu_shares => 512
}

输出:

{
  "HostConfig": {
    "Memory": 536870912,
    "MemoryReservation": 268435456,
    "NanoCpus": 1500000000,
    "CpuShares": 512
  }
}

值为 0 的资源字段会被省略。

Ulimits

输入:

[
    #{name => <<"nofile">>, soft => 1024, hard => 2048}
]

输出:

{
  "HostConfig": {
    "Ulimits": [
      {
        "Name": "nofile",
        "Soft": 1024,
        "Hard": 2048
      }
    ]
  }
}

Tmpfs

输入:

#{
    <<"/tmp">> => <<>>,
    <<"/run">> => <<"rw,size=64m">>
}

输出:

{
  "HostConfig": {
    "Tmpfs": {
      "/tmp": "",
      "/run": "rw,size=64m"
    }
  }
}

Sysctls

输入:

#{<<"net.ipv4.ip_forward">> => <<"1">>}

输出:

{
  "HostConfig": {
    "Sysctls": {
      "net.ipv4.ip_forward": "1"
    }
  }
}

ExtraHosts

输入:

[<<"host.docker.internal:host-gateway">>]

输出:

{
  "HostConfig": {
    "ExtraHosts": ["host.docker.internal:host-gateway"]
  }
}

10. create.networking_config 到 Docker JSON

输入:

#{
    endpoints => [
        #{name => <<"bridge">>},
        #{name => <<"mynet">>}
    ]
}

输出:

{
  "NetworkingConfig": {
    "EndpointsConfig": {
      "bridge": {},
      "mynet": {}
    }
  }
}

未传或 endpoints 为空时输出:

{
  "NetworkingConfig": {}
}

11. 完整转换示例

收到的 deploy command

{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;">>],
                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 示意:

{
  "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": [
      "/efka/root/my_nginx/service.conf:/usr/local/etc/service.conf",
      "/host/data:/data"
    ],
    "RestartPolicy": {
      "Name": "always"
    },
    "Memory": 536870912
  }
}

12. 部署流程补充

deploy action 的执行流程:

  1. 根据 container_nameefka.root_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 返回名称冲突,部署会失败并上报“本地容器已经存在”。