diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl index 34f32d8be..52cbc4775 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl @@ -778,7 +778,7 @@ to_bin(List) when is_list(List) -> to_bin(Boolean) when is_boolean(Boolean) -> Boolean; to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8); to_bin({Type, Args}) -> - unicode:characters_to_binary(io_lib:format("~p(~p)", [Type, Args])); + unicode:characters_to_binary(io_lib:format("~ts(~p)", [Type, Args])); to_bin(X) -> X. diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 31678e0f6..925c20ff1 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -96,6 +96,16 @@ schema("/listeners") -> listener_id_status_example() ) } + }, + post => #{ + tags => [<<"listeners">>], + desc => <<"Create the specified listener on all nodes.">>, + parameters => [], + 'requestBody' => create_listener_schema(#{bind => true}), + responses => #{ + 200 => listener_schema(#{bind => true}), + 400 => error_codes(['BAD_LISTENER_ID', 'BAD_REQUEST']) + } } }; schema("/listeners/:id") -> @@ -129,7 +139,8 @@ schema("/listeners/:id") -> responses => #{ 200 => listener_schema(#{bind => true}), 400 => error_codes(['BAD_LISTENER_ID', 'BAD_REQUEST']) - } + }, + deprecated => true }, delete => #{ tags => [<<"listeners">>], @@ -251,10 +262,10 @@ fields(node_status) -> })}, {status, ?HOCON(?R_REF(status))} ]; +fields({Type, with_name}) -> + listener_struct_with_name(Type); fields(Type) -> - Listeners = listeners_info(#{bind => true}) ++ listeners_info(#{bind => false}), - [Schema] = [S || #{ref := ?R_REF(_, T), schema := S} <- Listeners, T =:= Type], - Schema. + listener_struct(Type). listener_schema(Opts) -> emqx_dashboard_swagger:schema_with_example( @@ -262,6 +273,17 @@ listener_schema(Opts) -> tcp_schema_example() ). +create_listener_schema(Opts) -> + Schemas = [ + ?R_REF(Mod, {Type, with_name}) + || #{ref := ?R_REF(Mod, Type)} <- listeners_info(Opts) + ], + Example = maps:remove(id, tcp_schema_example()), + emqx_dashboard_swagger:schema_with_example( + ?UNION(Schemas), + Example#{name => <<"demo">>} + ). + listeners_type() -> lists:map( fun({Type, _}) -> list_to_existing_atom(Type) end, @@ -339,7 +361,9 @@ list_listeners(get, #{query_string := Query}) -> {ok, Type} -> listener_type_filter(atom_to_binary(Type), Listeners); error -> Listeners end, - {200, listener_status_by_id(NodeL)}. + {200, listener_status_by_id(NodeL)}; +list_listeners(post, #{body := Body}) -> + create_listener(Body). crud_listeners_by_id(get, #{bindings := #{id := Id0}}) -> Listeners = @@ -382,23 +406,8 @@ crud_listeners_by_id(put, #{bindings := #{id := Id}, body := Body0}) -> _ -> {400, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_ID_INCONSISTENT}} end; -crud_listeners_by_id(post, #{bindings := #{id := Id}, body := Body0}) -> - case parse_listener_conf(Body0) of - {Id, Type, Name, Conf} -> - Path = [listeners, Type, Name], - case create(Path, Conf) of - {ok, #{raw_config := _RawConf}} -> - crud_listeners_by_id(get, #{bindings => #{id => Id}}); - {error, already_exist} -> - {400, #{code => 'BAD_LISTENER_ID', message => <<"Already Exist">>}}; - {error, Reason} -> - {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}} - end; - {error, Reason} -> - {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}}; - _ -> - {400, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_ID_INCONSISTENT}} - end; +crud_listeners_by_id(post, #{body := Body}) -> + create_listener(Body); crud_listeners_by_id(delete, #{bindings := #{id := Id}}) -> {ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(Id), case ensure_remove([listeners, Type, Name]) of @@ -408,13 +417,24 @@ crud_listeners_by_id(delete, #{bindings := #{id := Id}}) -> parse_listener_conf(Conf0) -> Conf1 = maps:without([<<"running">>, <<"current_connections">>], Conf0), - {IdBin, Conf2} = maps:take(<<"id">>, Conf1), - {TypeBin, Conf3} = maps:take(<<"type">>, Conf2), - {ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(IdBin), + {TypeBin, Conf2} = maps:take(<<"type">>, Conf1), TypeAtom = binary_to_existing_atom(TypeBin), - case Type =:= TypeAtom of - true -> {binary_to_existing_atom(IdBin), TypeAtom, Name, Conf3}; - false -> {error, listener_type_inconsistent} + + case maps:take(<<"id">>, Conf2) of + {IdBin, Conf3} -> + {ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(IdBin), + case Type =:= TypeAtom of + true -> {binary_to_existing_atom(IdBin), TypeAtom, Name, Conf3}; + false -> {error, listener_type_inconsistent} + end; + _ -> + case maps:take(<<"name">>, Conf2) of + {Name, Conf3} -> + IdBin = <>, + {binary_to_atom(IdBin), TypeAtom, Name, Conf3}; + _ -> + {error, listener_config_invalid} + end end. stop_listeners_by_id(Method, Body = #{bindings := Bindings}) -> @@ -787,3 +807,37 @@ tcp_schema_example() -> type => tcp, zone => default }. + +create_listener(Body) -> + case parse_listener_conf(Body) of + {Id, Type, Name, Conf} -> + Path = [listeners, Type, Name], + case create(Path, Conf) of + {ok, #{raw_config := _RawConf}} -> + crud_listeners_by_id(get, #{bindings => #{id => Id}}); + {error, already_exist} -> + {400, #{code => 'BAD_LISTENER_ID', message => <<"Already Exist">>}}; + {error, Reason} -> + {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}} + end; + {error, Reason} -> + {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}} + end. + +listener_struct(Type) -> + Listeners = listeners_info(#{bind => true}) ++ listeners_info(#{bind => false}), + [Schema] = [S || #{ref := ?R_REF(_, T), schema := S} <- Listeners, T =:= Type], + Schema. + +listener_struct_with_name(Type) -> + BaseSchema = listener_struct(Type), + lists:keyreplace( + id, + 1, + BaseSchema, + {name, + ?HOCON(binary(), #{ + desc => "Listener name", + required => true + })} + ). diff --git a/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl index f72f9b762..10d04db85 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl @@ -37,6 +37,35 @@ t_list_listeners(_) -> Res = request(get, Path, [], []), #{<<"listeners">> := Expect} = emqx_mgmt_api_listeners:do_list_listeners(), ?assertEqual(length(Expect), length(Res)), + + %% POST /listeners + ListenerId = <<"tcp:default">>, + NewListenerId = <<"tcp:new">>, + + OriginPath = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]), + NewPath = emqx_mgmt_api_test_util:api_path(["listeners", NewListenerId]), + + OriginListener = request(get, OriginPath, [], []), + + %% create with full options + ?assertEqual({error, not_found}, is_running(NewListenerId)), + ?assertMatch({error, {"HTTP/1.1", 404, _}}, request(get, NewPath, [], [])), + + OriginListener2 = maps:remove(<<"id">>, OriginListener), + NewConf = OriginListener2#{ + <<"name">> => <<"new">>, + <<"bind">> => <<"0.0.0.0:2883">> + }, + Create = request(post, Path, [], NewConf), + ?assertEqual(lists:sort(maps:keys(OriginListener)), lists:sort(maps:keys(Create))), + Get1 = request(get, NewPath, [], []), + ?assertMatch(Create, Get1), + ?assert(is_running(NewListenerId)), + + %% delete + ?assertEqual([], delete(NewPath)), + ?assertEqual({error, not_found}, is_running(NewListenerId)), + ?assertMatch({error, {"HTTP/1.1", 404, _}}, request(get, NewPath, [], [])), ok. t_tcp_crud_listeners_by_id(_) ->