diff --git a/docs/container_deploy_request.md b/docs/container_deploy_request.md index e71f846..610a71b 100644 --- a/docs/container_deploy_request.md +++ b/docs/container_deploy_request.md @@ -4,8 +4,8 @@ 当前实现对应代码: -- 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) +- 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 请求格式 @@ -395,7 +395,13 @@ RestartPolicy { ## 5. 参数校验规则 -在 HTTP 层,`config` 会先经过基础类型校验。 +当前实现里: + +- `container_handler` 只校验顶层请求结构: + - `uuid` 必须是 `binary` + - `task_id` 必须是 `integer` + - `config` 必须是 `map` +- `config` 内部字段的必填项、类型检查、格式解析和不支持字段判断,全部在 `iot_container_request_builder:deploy_request/2` 中完成 必填字段: diff --git a/src/host/iot_container_request_builder.erl b/src/host/iot_container_request_builder.erl index ae47f53..575afc7 100644 --- a/src/host/iot_container_request_builder.erl +++ b/src/host/iot_container_request_builder.erl @@ -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 -> try ensure_supported_deploy_config(Config), + validate_deploy_config(Config), Params = build_container_deploy_params(Config), {ok, #'ContainerRequest'{action = {deploy, #'ContainerRequest.Deploy'{task_id = TaskId, params = Params}}}} catch @@ -75,6 +76,135 @@ ensure_supported_deploy_config(Config) when is_map(Config) -> throw({error, <<"unsupported container config keys: ", Unsupported/binary>>}) 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'(). build_container_deploy_params(Config) when is_map(Config) -> ContainerName = maps:get(<<"container_name">>, Config), diff --git a/src/transport/http/container_handler.erl b/src/transport/http/container_handler.erl index 1ec94bc..4ac34a3 100644 --- a/src/transport/http/container_handler.erl +++ b/src/transport/http/container_handler.erl @@ -62,28 +62,21 @@ handle_request("POST", "/container/push_config", _, %% 部署微服务 handle_request("POST", "/container/deploy", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId, <<"config">> := Config}) when is_binary(UUID), is_integer(TaskId), is_map(Config) -> - - case validate_config(Config) of - ok -> - case iot_host:get_pid(UUID) of - undefined -> - {ok, 200, iot_util:json_error(404, <<"host not found">>)}; - Pid when is_pid(Pid) -> - case iot_host:deploy_container(Pid, TaskId, Config) of - {ok, Ref} -> - case iot_host:await_reply(Pid, Ref, ?REQ_TIMEOUT) of - {ok, Result} -> - {ok, 200, rpc_success_response(Result)}; - {error, Code, Reason} -> - rpc_error_http_response(Code, Reason) - 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)} + case iot_host:get_pid(UUID) of + undefined -> + {ok, 200, iot_util:json_error(404, <<"host not found">>)}; + Pid when is_pid(Pid) -> + case iot_host:deploy_container(Pid, TaskId, Config) of + {ok, Ref} -> + case iot_host:await_reply(Pid, Ref, ?REQ_TIMEOUT) of + {ok, Result} -> + {ok, 200, rpc_success_response(Result)}; + {error, Code, Reason} -> + rpc_error_http_response(Code, Reason) + end; + {error, Reason} when is_binary(Reason) -> + {ok, 200, iot_util:json_error(400, Reason)} + end end; %% 启动服务 @@ -165,152 +158,6 @@ handle_request(_, Path, _, _) -> Path1 = list_to_binary(Path), {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) -> case decode_json_bytes(Result) of {ok, Data} ->