From 1fdd0bf5a58c025412c22efa40f61a6aba4fed75 Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Sun, 10 May 2026 23:44:35 +0800 Subject: [PATCH] fix ports --- docs/container_deploy_request.md | 40 +++++++++++--- src/docker/docker_container_builder.erl | 72 +++++++++++++++++++++++-- src/transport/ssl_channel.erl | 1 + 3 files changed, 102 insertions(+), 11 deletions(-) diff --git a/docs/container_deploy_request.md b/docs/container_deploy_request.md index 52ef405..00915d8 100644 --- a/docs/container_deploy_request.md +++ b/docs/container_deploy_request.md @@ -124,7 +124,8 @@ GET /event_stream?uuid=&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=&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=&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" } } ``` diff --git a/src/docker/docker_container_builder.erl b/src/docker/docker_container_builder.erl index a398dcd..49689ee 100644 --- a/src/docker/docker_container_builder.erl +++ b/src/docker/docker_container_builder.erl @@ -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) -> <>. --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), diff --git a/src/transport/ssl_channel.erl b/src/transport/ssl_channel.erl index a805052..ddb510b 100644 --- a/src/transport/ssl_channel.erl +++ b/src/transport/ssl_channel.erl @@ -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]),