This commit is contained in:
anlicheng 2026-04-19 15:09:31 +08:00
parent 251acb9b17
commit cb1e195a38
3 changed files with 154 additions and 171 deletions

View File

@ -4,8 +4,8 @@
当前实现对应代码: 当前实现对应代码:
- HTTP 入口校验[src/transport/http/container_handler.erl](/usr/local/code/cloudkit/iot/src/transport/http/container_handler.erl:63) - HTTP 入口:[src/transport/http/container_handler.erl](/usr/local/code/cloudkit/iot/src/transport/http/container_handler.erl:63)
- 请求构造器:[src/host/iot_container_request_builder.erl](/usr/local/code/cloudkit/iot/src/host/iot_container_request_builder.erl:25) - 请求构造器与 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) - protobuf 定义:[proto/message.proto](/usr/local/code/cloudkit/iot/proto/message.proto:37)
## 1. HTTP 请求格式 ## 1. HTTP 请求格式
@ -395,7 +395,13 @@ RestartPolicy {
## 5. 参数校验规则 ## 5. 参数校验规则
在 HTTP 层,`config` 会先经过基础类型校验。 当前实现里:
- `container_handler` 只校验顶层请求结构:
- `uuid` 必须是 `binary`
- `task_id` 必须是 `integer`
- `config` 必须是 `map`
- `config` 内部字段的必填项、类型检查、格式解析和不支持字段判断,全部在 `iot_container_request_builder:deploy_request/2` 中完成
必填字段: 必填字段:

View File

@ -27,6 +27,7 @@ config_request(ContainerName, ConfigJson) when is_binary(ContainerName), is_bina
deploy_request(TaskId, Config) when is_integer(TaskId), is_map(Config), TaskId >= 0 -> deploy_request(TaskId, Config) when is_integer(TaskId), is_map(Config), TaskId >= 0 ->
try try
ensure_supported_deploy_config(Config), ensure_supported_deploy_config(Config),
validate_deploy_config(Config),
Params = build_container_deploy_params(Config), Params = build_container_deploy_params(Config),
{ok, #'ContainerRequest'{action = {deploy, #'ContainerRequest.Deploy'{task_id = TaskId, params = Params}}}} {ok, #'ContainerRequest'{action = {deploy, #'ContainerRequest.Deploy'{task_id = TaskId, params = Params}}}}
catch catch
@ -75,6 +76,135 @@ ensure_supported_deploy_config(Config) when is_map(Config) ->
throw({error, <<"unsupported container config keys: ", Unsupported/binary>>}) throw({error, <<"unsupported container config keys: ", Unsupported/binary>>})
end. end.
-spec validate_deploy_config(Config :: map()) -> ok.
validate_deploy_config(Config) when is_map(Config) ->
Required = [
{<<"image">>, binary},
{<<"container_name">>, binary},
{<<"command">>, {list, binary}},
{<<"restart">>, binary}
],
Optional = [
{<<"privileged">>, boolean},
{<<"entrypoint">>, {list, binary}},
{<<"envs">>, {list, binary}},
{<<"ports">>, {list, binary}},
{<<"expose">>, {list, binary}},
{<<"volumes">>, {list, binary}},
{<<"networks">>, {list, binary}},
{<<"labels">>, {map, {binary, binary}}},
{<<"user">>, binary},
{<<"working_dir">>, binary},
{<<"hostname">>, binary},
{<<"container_dir">>, binary},
{<<"network_mode">>, binary},
{<<"cap_add">>, {list, binary}},
{<<"cap_drop">>, {list, binary}},
{<<"devices">>, {list, binary}},
{<<"mem_limit">>, binary},
{<<"mem_reservation">>, binary},
{<<"cpu_shares">>, integer},
{<<"cpus">>, number},
{<<"ulimits">>, {map, {binary, binary}}},
{<<"sysctls">>, {map, {binary, binary}}},
{<<"tmpfs">>, {list, binary}},
{<<"extra_hosts">>, {list, binary}},
{<<"healthcheck">>, {map, {binary, any}}}
],
Errors = check_required(Config, Required) ++ check_optional(Config, Optional),
case Errors of
[] ->
ok;
_ ->
throw({error, iolist_to_binary(lists:join(<<"|||">>, Errors))})
end.
-spec check_required(map(), list()) -> [binary()].
check_required(Config, Fields) ->
lists:foldl(
fun({Key, Type}, ErrAcc) ->
case maps:get(Key, Config, undefined) of
undefined ->
[iolist_to_binary(io_lib:format("miss requied parameter: ~p", [Key])) | ErrAcc];
Value ->
case check_type(Value, Type) of
true ->
ErrAcc;
false ->
[iolist_to_binary(io_lib:format("required parameter: ~p, type must be: ~ts", [Key, type_name(Type)])) | ErrAcc]
end
end
end,
[], Fields).
-spec check_optional(map(), list()) -> [binary()].
check_optional(Config, Fields) ->
lists:foldl(
fun({Key, Type}, ErrAcc) ->
case maps:get(Key, Config, undefined) of
undefined ->
ErrAcc;
Value ->
case check_type(Value, Type) of
true ->
ErrAcc;
false ->
[iolist_to_binary(io_lib:format("optional parameter: ~p, type must be: ~ts", [Key, type_name(Type)])) | ErrAcc]
end
end
end,
[], Fields).
-spec type_name(tuple() | atom()) -> binary().
type_name(binary) ->
<<"string">>;
type_name(integer) ->
<<"integer">>;
type_name(number) ->
<<"number">>;
type_name(list) ->
<<"list">>;
type_name({list, binary}) ->
<<"list of string">>;
type_name({list, number}) ->
<<"list of number">>;
type_name({list, integer}) ->
<<"list of integer">>;
type_name(map) ->
<<"map">>;
type_name({map, {binary, binary}}) ->
<<"map of string:string">>;
type_name({map, {binary, any}}) ->
<<"map of string:any">>;
type_name(boolean) ->
<<"boolean">>.
-spec check_type(Value :: any(), any()) -> boolean().
check_type(Value, binary) ->
is_binary(Value);
check_type(Value, integer) ->
is_integer(Value);
check_type(Value, number) ->
is_number(Value);
check_type(Value, list) when is_list(Value) ->
true;
check_type(Value, {list, binary}) when is_list(Value) ->
lists:all(fun(E) -> is_binary(E) end, Value);
check_type(Value, {list, number}) when is_list(Value) ->
lists:all(fun(E) -> is_number(E) end, Value);
check_type(Value, {list, integer}) when is_list(Value) ->
lists:all(fun(E) -> is_integer(E) end, Value);
check_type(Value, map) when is_map(Value) ->
true;
check_type(Value, {map, {binary, binary}}) when is_map(Value) ->
lists:all(fun({K, V}) -> is_binary(K) andalso is_binary(V) end, maps:to_list(Value));
check_type(Value, {map, {binary, any}}) when is_map(Value) ->
lists:all(fun({K, _}) -> is_binary(K) end, maps:to_list(Value));
check_type(Value, boolean) ->
is_boolean(Value);
check_type(_, _) ->
false.
-spec build_container_deploy_params(Config :: map()) -> message_pb:'ContainerDeployParams'(). -spec build_container_deploy_params(Config :: map()) -> message_pb:'ContainerDeployParams'().
build_container_deploy_params(Config) when is_map(Config) -> build_container_deploy_params(Config) when is_map(Config) ->
ContainerName = maps:get(<<"container_name">>, Config), ContainerName = maps:get(<<"container_name">>, Config),

View File

@ -62,28 +62,21 @@ handle_request("POST", "/container/push_config", _,
%% %%
handle_request("POST", "/container/deploy", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId, <<"config">> := Config}) handle_request("POST", "/container/deploy", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId, <<"config">> := Config})
when is_binary(UUID), is_integer(TaskId), is_map(Config) -> when is_binary(UUID), is_integer(TaskId), is_map(Config) ->
case iot_host:get_pid(UUID) of
case validate_config(Config) of undefined ->
ok -> {ok, 200, iot_util:json_error(404, <<"host not found">>)};
case iot_host:get_pid(UUID) of Pid when is_pid(Pid) ->
undefined -> case iot_host:deploy_container(Pid, TaskId, Config) of
{ok, 200, iot_util:json_error(404, <<"host not found">>)}; {ok, Ref} ->
Pid when is_pid(Pid) -> case iot_host:await_reply(Pid, Ref, ?REQ_TIMEOUT) of
case iot_host:deploy_container(Pid, TaskId, Config) of {ok, Result} ->
{ok, Ref} -> {ok, 200, rpc_success_response(Result)};
case iot_host:await_reply(Pid, Ref, ?REQ_TIMEOUT) of {error, Code, Reason} ->
{ok, Result} -> rpc_error_http_response(Code, Reason)
{ok, 200, rpc_success_response(Result)}; end;
{error, Code, Reason} -> {error, Reason} when is_binary(Reason) ->
rpc_error_http_response(Code, Reason) {ok, 200, iot_util:json_error(400, Reason)}
end; end
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
{error, Errors} ->
Reason = iolist_to_binary(lists:join(<<"|||">>, Errors)),
{ok, 200, iot_util:json_error(400, Reason)}
end; end;
%% %%
@ -165,152 +158,6 @@ handle_request(_, Path, _, _) ->
Path1 = list_to_binary(Path), Path1 = list_to_binary(Path),
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}. {ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% helper methods
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
validate_config(Config) when is_map(Config) ->
%%
Required = [
{<<"image">>, binary},
{<<"container_name">>, binary},
{<<"command">>, {list, binary}},
{<<"restart">>, binary}
],
%%
Optional = [
{<<"privileged">>, boolean},
{<<"entrypoint">>, {list, binary}},
{<<"envs">>, {list, binary}},
{<<"ports">>, {list, binary}},
{<<"expose">>, {list, binary}},
{<<"volumes">>, {list, binary}},
{<<"networks">>, {list, binary}},
{<<"labels">>, {map, {binary, binary}}},
{<<"user">>, binary},
{<<"working_dir">>, binary},
{<<"hostname">>, binary},
{<<"container_dir">>, binary},
{<<"network_mode">>, binary},
{<<"cap_add">>, {list, binary}},
{<<"cap_drop">>, {list, binary}},
{<<"devices">>, {list, binary}},
{<<"mem_limit">>, binary},
{<<"mem_reservation">>, binary},
{<<"cpu_shares">>, integer},
{<<"cpus">>, number},
{<<"ulimits">>, {map, {binary, binary}}},
{<<"sysctls">>, {map, {binary, binary}}},
{<<"tmpfs">>, {list, binary}},
{<<"extra_hosts">>, {list, binary}},
{<<"healthcheck">>, {map, {binary, any}}}
],
Errors1 = check_required(Config, Required),
Errors2 = check_optional(Config, Optional),
Errors = Errors1 ++ Errors2,
case Errors of
[] ->
ok;
_ ->
{error, lists:map(fun erlang:iolist_to_binary/1, Errors)}
end.
%%------------------------------------------------------------------------------
%%
%%------------------------------------------------------------------------------
check_required(Config, Fields) ->
lists:foldl(
fun({Key, Type}, ErrAcc) ->
case maps:get(Key, Config, undefined) of
undefined ->
[io_lib:format("miss requied parameter: ~p", [Key]) | ErrAcc];
Value ->
case check_type(Value, Type) of
true ->
ErrAcc;
false ->
[io_lib:format("required parameter: ~p, type must be: ~p", [Key, type_name(Type)]) | ErrAcc]
end
end
end,
[], Fields).
%%------------------------------------------------------------------------------
%%
%%------------------------------------------------------------------------------
check_optional(Config, Fields) ->
lists:foldl(
fun({Key, Type}, ErrAcc) ->
case maps:get(Key, Config, undefined) of
undefined ->
ErrAcc;
Value ->
case check_type(Value, Type) of
true ->
ErrAcc;
false ->
[io_lib:format("optional parameter: ~p, type must be: ~p", [Key, type_name(Type)]) | ErrAcc]
end
end
end,
[], Fields).
%%------------------------------------------------------------------------------
%% binary版
%%------------------------------------------------------------------------------
-spec type_name(tuple() | atom()) -> binary().
type_name(binary) ->
<<"string">>;
type_name(integer) ->
<<"integer">>;
type_name(number) ->
<<"number">>;
type_name(list) ->
<<"list">>;
type_name({list, binary}) ->
<<"list of string">>;
type_name({list, number}) ->
<<"list of number">>;
type_name({list, integer}) ->
<<"list of integer">>;
type_name(map) ->
<<"map">>;
type_name({map, {binary, binary}}) ->
<<"map of string:string">>;
type_name({map, {binary, any}}) ->
<<"map of string:any">>;
type_name(boolean) ->
<<"boolean">>.
-spec check_type(Value :: any(), any()) -> boolean().
check_type(Value, binary) ->
is_binary(Value);
check_type(Value, integer) ->
is_integer(Value);
check_type(Value, number) ->
is_number(Value);
check_type(Value, list) when is_list(Value) ->
true;
check_type(Value, {list, binary}) when is_list(Value) ->
lists:all(fun(E) -> is_binary(E) end, Value);
check_type(Value, {list, number}) when is_list(Value) ->
lists:all(fun(E) -> is_number(E) end, Value);
check_type(Value, {list, integer}) when is_list(Value) ->
lists:all(fun(E) -> is_integer(E) end, Value);
check_type(Value, map) when is_map(Value) ->
true;
check_type(Value, {map, {binary, binary}}) when is_map(Value) ->
lists:all(fun({K, V}) -> is_binary(K) andalso is_binary(V) end, maps:to_list(Value));
check_type(Value, {map, {binary, any}}) when is_map(Value) ->
lists:all(fun({K, _}) -> is_binary(K) end, maps:to_list(Value));
check_type(Value, boolean) ->
is_boolean(Value);
check_type(_, _) ->
false.
rpc_success_response(Result) when is_binary(Result) -> rpc_success_response(Result) when is_binary(Result) ->
case decode_json_bytes(Result) of case decode_json_bytes(Result) of
{ok, Data} -> {ok, Data} ->