fix ports

This commit is contained in:
anlicheng 2026-05-10 23:44:35 +08:00
parent a85b38748f
commit 1fdd0bf5a5
3 changed files with 102 additions and 11 deletions

View File

@ -124,7 +124,8 @@ GET /event_stream?uuid=<host_uuid>&task_id=<task_id>
| --- | --- | --- | --- |
| `entrypoint` | string[] | `[]` | Docker create config 的 `entrypoint`。 |
| `envs` | string[] | `[]` | 环境变量列表,例如 `["A=1"]`,对应 Docker create config 的 `env`。 |
| `expose` | string[] | `[]` | 容器暴露端口,只表示容器端口,不支持宿主机端口绑定。 |
| `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`。 |
@ -150,10 +151,9 @@ GET /event_stream?uuid=<host_uuid>&task_id=<task_id>
| 字段 | 当前行为 | 说明 |
| --- | --- | --- |
| `ports` | 明确拒绝 | 如果传入会返回 `unsupported container config keys: ports`。当前只支持 `expose`,不支持宿主机端口绑定。 |
| `container_dir` | 明确拒绝 | 容器目录由 efka 按系统默认规则管理HTTP 调用方不能指定 efka 主机上的部署目录。 |
| `env_file` | 忽略 | 当前校验不会识别该字段,后续构造 Docker create options 时也不会使用。 |
| 其他未知字段 | 忽略 | 除 `ports` 和 `container_dir` 外,未知字段不会报错,也不会进入内部部署参数。 |
| 其他未知字段 | 忽略 | 除 `container_dir` 外,未知字段不会报错,也不会进入内部部署参数。 |
## 5. 校验规则
@ -167,7 +167,7 @@ GET /event_stream?uuid=<host_uuid>&task_id=<task_id>
第二层在 `docker_container_builder:deploy_request/2`
- 先拒绝不支持的 `ports` 和 `container_dir` 字段。
- 先拒绝不支持的 `container_dir` 字段。
- 检查必填字段是否存在。
- 检查已知字段类型。
- 检查 `healthcheck` 内部字段类型,避免无效嵌套字段进入构造阶段。
@ -243,7 +243,7 @@ HTTP handler 最多等待 10 秒。超时返回 HTTP 504其他参数或执行
| `user` | `user` | 默认 `""`。 |
| `working_dir` | `working_dir` | 默认 `""`。 |
| `hostname` | `hostname` | 默认 `""`。 |
| `exposed_ports` | `expose` | 转成 `#{container_port, protocol}` map 列表。 |
| `exposed_ports` | `expose` + `ports` | 转成 `#{container_port, protocol}` map 列表`ports` 中的容器端口会自动补进 `exposed_ports`。 |
| `healthcheck` | `healthcheck` | 未传时为 `undefined`。 |
## 8. Docker host config 映射
@ -263,6 +263,7 @@ HTTP handler 最多等待 10 秒。超时返回 HTTP 504其他参数或执行
| `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` | 默认 `{}`。 |
@ -339,6 +340,32 @@ HTTP handler 最多等待 10 秒。超时返回 HTTP 504其他参数或执行
端口必须是无符号整数,且不能超过 `4294967295`
### ports
输入:
```json
["8080:80", "443:443", "8053:53/udp"]
```
转换为 `create.host_config.port_bindings`
```erlang
[
#{<<"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
输入:
@ -599,6 +626,7 @@ HTTP handler 最多等待 10 秒。超时返回 HTTP 504其他参数或执行
<<"memory_reservation">> => 0,
<<"nano_cpus">> => 0,
<<"cpu_shares">> => 0,
<<"port_bindings">> => [],
<<"ulimits">> => [],
<<"tmpfs">> => #{},
<<"sysctls">> => #{},
@ -628,7 +656,7 @@ HTTP handler 最多等待 10 秒。超时返回 HTTP 504其他参数或执行
{
"error": {
"code": 400,
"message": "unsupported container config keys: ports"
"message": "invalid port binding"
}
}
```

View File

@ -72,7 +72,7 @@ container_ref(ContainerName) when is_binary(ContainerName) ->
-spec ensure_supported_deploy_config(map()) -> ok.
ensure_supported_deploy_config(Config) when is_map(Config) ->
UnsupportedKeys = [Key || Key <- [<<"ports">>, <<"container_dir">>], maps:is_key(Key, Config)],
UnsupportedKeys = [Key || Key <- [<<"container_dir">>], maps:is_key(Key, Config)],
case UnsupportedKeys of
[] ->
ok;
@ -267,7 +267,7 @@ build_docker_container_config(Config) when is_map(Config) ->
user => maps:get(<<"user">>, Config, <<>>),
working_dir => maps:get(<<"working_dir">>, Config, <<>>),
hostname => maps:get(<<"hostname">>, Config, <<>>),
exposed_ports => build_exposed_ports(maps:get(<<"expose">>, Config, [])),
exposed_ports => build_exposed_ports(Config),
healthcheck => build_healthcheck(maps:get(<<"healthcheck">>, Config, undefined))
}.
@ -285,6 +285,7 @@ build_docker_host_config(Config) when is_map(Config) ->
memory_reservation => default_uint64(parse_optional_size_bytes(maps:get(<<"mem_reservation">>, Config, undefined), <<"mem_reservation">>)),
nano_cpus => default_uint64(parse_optional_nano_cpus(maps:get(<<"cpus">>, Config, undefined))),
cpu_shares => default_uint64(maps:get(<<"cpu_shares">>, Config, undefined)),
port_bindings => build_port_bindings(maps:get(<<"ports">>, Config, [])),
ulimits => build_ulimits(maps:get(<<"ulimits">>, Config, #{})),
tmpfs => maps:from_list(build_tmpfs_options(maps:get(<<"tmpfs">>, Config, []))),
sysctls => maps:get(<<"sysctls">>, Config, #{}),
@ -357,9 +358,14 @@ volume_bind(HostPath, ContainerPath, true) when is_binary(HostPath), is_binary(C
volume_bind(HostPath, ContainerPath, false) when is_binary(HostPath), is_binary(ContainerPath) ->
<<HostPath/binary, ":", ContainerPath/binary>>.
-spec build_exposed_ports([binary()]) -> [map()].
build_exposed_ports(ExposeSpecs) when is_list(ExposeSpecs) ->
[build_exposed_port(ExposeSpec) || ExposeSpec <- ExposeSpecs].
-spec build_exposed_ports(map()) -> [map()].
build_exposed_ports(Config) when is_map(Config) ->
ExposePorts = [build_exposed_port(ExposeSpec) || ExposeSpec <- maps:get(<<"expose">>, Config, [])],
BoundPorts = [
#{container_port => ContainerPort, protocol => Protocol} ||
#{container_port := ContainerPort, protocol := Protocol} <- build_port_bindings(maps:get(<<"ports">>, Config, []))
],
unique_ports(ExposePorts ++ BoundPorts).
-spec build_exposed_port(binary()) -> map().
build_exposed_port(ExposeSpec) when is_binary(ExposeSpec) ->
@ -370,6 +376,52 @@ build_exposed_port(ExposeSpec) when is_binary(ExposeSpec) ->
#{container_port => parse_uint32(PortBin, <<"expose">>), protocol => Protocol}
end.
-spec build_port_bindings([binary()]) -> [map()].
build_port_bindings(PortSpecs) when is_list(PortSpecs) ->
[build_port_binding(PortSpec) || PortSpec <- PortSpecs].
-spec build_port_binding(binary()) -> map().
build_port_binding(PortSpec) when is_binary(PortSpec) ->
case binary:split(PortSpec, <<":">>, [global]) of
[HostPortBin, ContainerPortSpec] when HostPortBin =/= <<>>, ContainerPortSpec =/= <<>> ->
{ContainerPort, Protocol} = parse_container_port_spec(ContainerPortSpec, <<"ports">>),
#{
host_ip => <<>>,
host_port => parse_tcp_port(HostPortBin, <<"ports.host_port">>),
container_port => ContainerPort,
protocol => Protocol
};
_ ->
throw({error, <<"invalid port binding">>})
end.
-spec parse_container_port_spec(binary(), binary()) -> {non_neg_integer(), binary()}.
parse_container_port_spec(PortSpec, Field) when is_binary(PortSpec), is_binary(Field) ->
case binary:split(PortSpec, <<"/">>) of
[PortBin] ->
{parse_tcp_port(PortBin, Field), <<"tcp">>};
[PortBin, Protocol] when Protocol =/= <<>> ->
{parse_tcp_port(PortBin, Field), Protocol};
_ ->
throw({error, <<"invalid port binding">>})
end.
-spec unique_ports([map()]) -> [map()].
unique_ports(Ports) when is_list(Ports) ->
{_Seen, Result} = lists:foldl(
fun(Port = #{container_port := ContainerPort, protocol := Protocol}, {Seen, Acc}) ->
Key = {ContainerPort, Protocol},
case maps:is_key(Key, Seen) of
true ->
{Seen, Acc};
false ->
{Seen#{Key => true}, [Port | Acc]}
end
end,
{#{}, []},
Ports),
lists:reverse(Result).
-spec build_device_mappings([binary()]) -> [map()].
build_device_mappings(DeviceSpecs) when is_list(DeviceSpecs) ->
[build_device_mapping(DeviceSpec) || DeviceSpec <- DeviceSpecs].
@ -479,6 +531,16 @@ parse_uint32(Value, Field) when is_binary(Value), is_binary(Field) ->
throw({error, <<"value overflow for ", Field/binary>>})
end.
-spec parse_tcp_port(binary(), binary()) -> non_neg_integer().
parse_tcp_port(Value, Field) when is_binary(Value), is_binary(Field) ->
Parsed = parse_uint32(Value, Field),
case Parsed =< 65535 of
true ->
Parsed;
false ->
throw({error, <<"port out of range for ", Field/binary>>})
end.
-spec parse_uint64(binary(), binary()) -> non_neg_integer().
parse_uint64(Value0, Field) when is_binary(Value0), is_binary(Field) ->
Value = trim_binary(Value0),

View File

@ -240,6 +240,7 @@ handle_message_frame(Body, State) ->
-spec handle_event_stream_frame(binary(), map()) -> ok.
handle_event_stream_frame(UUID, #{<<"task_id">> := TaskId, <<"type">> := <<"close">>, <<"stream">> := Reason}) ->
logger:debug("[ssl_channel] get uuid: ~p, task_id: ~p, close with reason: ~p", [UUID, TaskId, Reason]),
iot_container_task_sup:close(UUID, TaskId, Reason);
handle_event_stream_frame(UUID, #{<<"task_id">> := TaskId, <<"type">> := Type, <<"stream">> := Stream}) ->
logger:debug("[ssl_channel] get uuid: ~p, task_id: ~p, type: ~ts, stream: ~ts", [UUID, TaskId, Type, Stream]),