From 96ca0d9f499b591152241921e69feb1dd524dade Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Tue, 20 Dec 2022 16:12:13 +0100 Subject: [PATCH] feat(emqx_bridge): add /bridges_probe API endpoint --- apps/emqx_bridge/i18n/emqx_bridge_api.conf | 16 ++++ apps/emqx_bridge/src/emqx_bridge_api.erl | 76 ++++++++++++++++++- apps/emqx_bridge/src/emqx_bridge_resource.erl | 6 +- .../test/emqx_bridge_api_SUITE.erl | 46 +++++++++++ 4 files changed, 140 insertions(+), 4 deletions(-) diff --git a/apps/emqx_bridge/i18n/emqx_bridge_api.conf b/apps/emqx_bridge/i18n/emqx_bridge_api.conf index e8bb2403a..28f3db324 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_api.conf +++ b/apps/emqx_bridge/i18n/emqx_bridge_api.conf @@ -134,4 +134,20 @@ NOTE:不允许在单节点上启用/禁用 Bridge""" } } + desc_api9 { + desc { + en: """ +Test creating a new bridge by given ID
+The ID must be of format '{type}:{name}' +""" + zh: """ +通过给定的 ID 测试创建一个新的桥接。
+ID 的格式必须为 ’{type}:{name}” +""" + } + label: { + en: "Test Bridge Creation" + zh: "测试桥接创建" + } + } } diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index 0d0d2e9ad..aa11ebc5d 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -38,7 +38,8 @@ '/bridges/:id'/2, '/bridges/:id/operation/:operation'/2, '/nodes/:node/bridges/:id/operation/:operation'/2, - '/bridges/:id/reset_metrics'/2 + '/bridges/:id/reset_metrics'/2, + '/bridges_probe'/2 ]). -export([lookup_from_local_node/2]). @@ -68,7 +69,8 @@ paths() -> "/bridges/:id", "/bridges/:id/operation/:operation", "/nodes/:node/bridges/:id/operation/:operation", - "/bridges/:id/reset_metrics" + "/bridges/:id/reset_metrics", + "/bridges_probe" ]. error_schema(Code, Message) when is_atom(Code) -> @@ -384,6 +386,23 @@ schema("/nodes/:node/bridges/:id/operation/:operation") -> 503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable") } } + }; +schema("/bridges_probe") -> + #{ + 'operationId' => '/bridges_probe', + post => #{ + tags => [<<"bridges">>], + desc => ?DESC("desc_api9"), + summary => <<"Test creating bridge">>, + 'requestBody' => emqx_dashboard_swagger:schema_with_examples( + emqx_bridge_schema:post_request(), + bridge_info_examples(post) + ), + responses => #{ + 204 => <<"Test bridge OK">>, + 400 => error_schema(['TEST_FAILED'], "bridge test failed") + } + } }. '/bridges'(post, #{body := #{<<"type">> := BridgeType, <<"name">> := BridgeName} = Conf0}) -> @@ -462,6 +481,59 @@ schema("/nodes/:node/bridges/:id/operation/:operation") -> end ). +'/bridges_probe'(post, Request) -> + RequestMeta = #{module => ?MODULE, method => post, path => "/bridges_probe"}, + case emqx_dashboard_swagger:filter_check_request_and_translate_body(Request, RequestMeta) of + {ok, #{body := #{<<"type">> := ConnType} = Params}} -> + case do_probe(ConnType, maps:remove(<<"type">>, Params)) of + ok -> + {204}; + {error, Error} -> + {400, error_msg('TEST_FAILED', Error)} + end; + BadRequest -> + BadRequest + end. + +do_probe(ConnType, Params) -> + case test_connection(host_and_port(ConnType, Params)) of + ok -> + emqx_bridge_resource:create_dry_run(ConnType, Params); + Error -> + Error + end. + +host_and_port(mqtt, #{<<"server">> := Server}) -> + Server; +host_and_port(webhook, #{<<"url">> := Url}) -> + {BaseUrl, _Path} = parse_url(Url), + {ok, #{host := Host, port := Port}} = emqx_http_lib:uri_parse(BaseUrl), + {Host, Port}; +host_and_port(_Unknown, _) -> + undefined. + +test_connection(undefined) -> + %% be friendly, it might fail later on with a 'timeout' error. + ok; +test_connection({Host, Port}) -> + case gen_tcp:connect(Host, Port, []) of + {ok, TestSocket} -> gen_tcp:close(TestSocket); + Error -> Error + end. + +parse_url(Url) -> + case string:split(Url, "//", leading) of + [Scheme, UrlRem] -> + case string:split(UrlRem, "/", leading) of + [HostPort, Path] -> + {iolist_to_binary([Scheme, "//", HostPort]), Path}; + [HostPort] -> + {iolist_to_binary([Scheme, "//", HostPort]), <<>>} + end; + [Url] -> + error({invalid_url, Url}) + end. + lookup_from_all_nodes(BridgeType, BridgeName, SuccCode) -> Nodes = mria_mnesia:running_nodes(), case is_ok(emqx_bridge_proto_v1:lookup_from_all_nodes(Nodes, BridgeType, BridgeName)) of diff --git a/apps/emqx_bridge/src/emqx_bridge_resource.erl b/apps/emqx_bridge/src/emqx_bridge_resource.erl index d1ce260c9..dce7b9f1a 100644 --- a/apps/emqx_bridge/src/emqx_bridge_resource.erl +++ b/apps/emqx_bridge/src/emqx_bridge_resource.erl @@ -213,14 +213,16 @@ recreate(Type, Name, Conf, Opts) -> Opts ). -create_dry_run(Type, Conf) -> +create_dry_run(Type, Conf0) -> TmpPath = iolist_to_binary(["bridges-create-dry-run:", emqx_misc:gen_id(8)]), + Conf = emqx_map_lib:safe_atom_key_map(Conf0), case emqx_connector_ssl:convert_certs(TmpPath, Conf) of {error, Reason} -> {error, Reason}; {ok, ConfNew} -> + ParseConf = parse_confs(bin(Type), TmpPath, ConfNew), Res = emqx_resource:create_dry_run_local( - bridge_to_resource_type(Type), ConfNew + bridge_to_resource_type(Type), ParseConf ), _ = maybe_clear_certs(TmpPath, ConfNew), Res diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index e533c78b0..a32019e41 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -596,6 +596,52 @@ t_with_redact_update(_Config) -> ?assertEqual(Password, Value), ok. +-define(MQTT_BRIDGE(Server), #{ + <<"server">> => Server, + <<"username">> => <<"user1">>, + <<"password">> => <<"">>, + <<"proto_ver">> => <<"v5">>, + <<"ssl">> => #{<<"enable">> => false}, + <<"type">> => <<"mqtt">>, + <<"name">> => <<"mqtt_egress_test_bridge">> +}). + +t_bridges_probe(Config) -> + Port = ?config(port, Config), + URL = ?URL(Port, "some_path"), + + {ok, 204, <<>>} = request( + post, + uri(["bridges_probe"]), + ?HTTP_BRIDGE(URL, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ), + + %% second time with same name is ok since no real bridge created + {ok, 204, <<>>} = request( + post, + uri(["bridges_probe"]), + ?HTTP_BRIDGE(URL, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ), + + {ok, 400, _} = request( + post, + uri(["bridges_probe"]), + ?HTTP_BRIDGE(<<"http://203.0.113.3:1234/foo">>, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ), + + {ok, 204, _} = request( + post, + uri(["bridges_probe"]), + ?MQTT_BRIDGE(<<"127.0.0.1:1883">>) + ), + + {ok, 400, _} = request( + post, + uri(["bridges_probe"]), + ?MQTT_BRIDGE(<<"127.0.0.1:2883">>) + ), + ok. + request(Method, Url, Body) -> request(<<"bridge_admin">>, Method, Url, Body).