|
|
@@ -15,128 +15,194 @@
|
|
|
%%--------------------------------------------------------------------
|
|
|
-module(emqx_bridge_api).
|
|
|
|
|
|
--rest_api(#{ name => list_data_bridges
|
|
|
- , method => 'GET'
|
|
|
- , path => "/data_bridges"
|
|
|
- , func => list_bridges
|
|
|
- , descr => "List all data bridges"
|
|
|
- }).
|
|
|
-
|
|
|
--rest_api(#{ name => get_data_bridge
|
|
|
- , method => 'GET'
|
|
|
- , path => "/data_bridges/:bin:name"
|
|
|
- , func => get_bridge
|
|
|
- , descr => "Get a data bridge by name"
|
|
|
- }).
|
|
|
-
|
|
|
--rest_api(#{ name => create_data_bridge
|
|
|
- , method => 'POST'
|
|
|
- , path => "/data_bridges/:bin:name"
|
|
|
- , func => create_bridge
|
|
|
- , descr => "Create a new data bridge"
|
|
|
- }).
|
|
|
-
|
|
|
--rest_api(#{ name => update_data_bridge
|
|
|
- , method => 'PUT'
|
|
|
- , path => "/data_bridges/:bin:name"
|
|
|
- , func => update_bridge
|
|
|
- , descr => "Update an existing data bridge"
|
|
|
- }).
|
|
|
-
|
|
|
--rest_api(#{ name => delete_data_bridge
|
|
|
- , method => 'DELETE'
|
|
|
- , path => "/data_bridges/:bin:name"
|
|
|
- , func => delete_bridge
|
|
|
- , descr => "Delete an existing data bridge"
|
|
|
- }).
|
|
|
+-behaviour(minirest_api).
|
|
|
+
|
|
|
+-export([api_spec/0]).
|
|
|
|
|
|
-export([ list_bridges/2
|
|
|
- , get_bridge/2
|
|
|
- , create_bridge/2
|
|
|
- , update_bridge/2
|
|
|
- , delete_bridge/2
|
|
|
+ , list_local_bridges/1
|
|
|
+ , crud_bridges_cluster/2
|
|
|
+ , crud_bridges/3
|
|
|
]).
|
|
|
|
|
|
--define(BRIDGE(N, T, C), #{<<"name">> => N, <<"type">> => T, <<"config">> => C}).
|
|
|
-
|
|
|
-list_bridges(_Binding, _Params) ->
|
|
|
- {200, #{code => 0, data => [format_api_reply(Data) ||
|
|
|
- Data <- emqx_bridge:list_bridges()]}}.
|
|
|
-
|
|
|
-get_bridge(#{name := Name}, _Params) ->
|
|
|
- case emqx_resource:get_instance(emqx_bridge:name_to_resource_id(Name)) of
|
|
|
- {ok, Data} ->
|
|
|
- {200, #{code => 0, data => format_api_reply(emqx_resource_api:format_data(Data))}};
|
|
|
- {error, not_found} ->
|
|
|
- {404, #{code => 102, message => <<"not_found: ", Name/binary>>}}
|
|
|
+-define(TYPES, [mqtt]).
|
|
|
+-define(BRIDGE(N, T, C), #{<<"id">> => N, <<"type">> => T, <<"config">> => C}).
|
|
|
+-define(TRY_PARSE_ID(ID, EXPR),
|
|
|
+ try emqx_bridge:parse_bridge_id(Id) of
|
|
|
+ {BridgeType, BridgeName} -> EXPR
|
|
|
+ catch
|
|
|
+ error:{invalid_bridge_id, Id0} ->
|
|
|
+ {400, #{code => 102, message => <<"invalid_bridge_id: ", Id0/binary>>}}
|
|
|
+ end).
|
|
|
+
|
|
|
+req_schema() ->
|
|
|
+ Schema = [
|
|
|
+ case maps:to_list(emqx:get_raw_config([bridges, T], #{})) of
|
|
|
+ %% the bridge is not configured, so we have no method to get the schema
|
|
|
+ [] -> #{};
|
|
|
+ [{_K, Conf} | _] ->
|
|
|
+ emqx_mgmt_api_configs:gen_schema(Conf)
|
|
|
+ end
|
|
|
+ || T <- ?TYPES],
|
|
|
+ #{oneOf => Schema}.
|
|
|
+
|
|
|
+resp_schema() ->
|
|
|
+ #{oneOf := Schema} = req_schema(),
|
|
|
+ AddMetadata = fun(Prop) ->
|
|
|
+ Prop#{is_connected => #{type => boolean},
|
|
|
+ id => #{type => string},
|
|
|
+ bridge_type => #{type => string, enum => ?TYPES},
|
|
|
+ node => #{type => string}}
|
|
|
+ end,
|
|
|
+ Schema1 = [S#{properties => AddMetadata(Prop)}
|
|
|
+ || S = #{properties := Prop} <- Schema],
|
|
|
+ #{oneOf => Schema1}.
|
|
|
+
|
|
|
+api_spec() ->
|
|
|
+ {bridge_apis(), []}.
|
|
|
+
|
|
|
+bridge_apis() ->
|
|
|
+ [list_all_bridges_api(), crud_bridges_apis(), operation_apis()].
|
|
|
+
|
|
|
+list_all_bridges_api() ->
|
|
|
+ Metadata = #{
|
|
|
+ get => #{
|
|
|
+ description => <<"List all created bridges">>,
|
|
|
+ responses => #{
|
|
|
+ <<"200">> => emqx_mgmt_util:array_schema(resp_schema(),
|
|
|
+ <<"A list of the bridges">>)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {"/bridges/", Metadata, list_bridges}.
|
|
|
+
|
|
|
+crud_bridges_apis() ->
|
|
|
+ ReqSchema = req_schema(),
|
|
|
+ RespSchema = resp_schema(),
|
|
|
+ Metadata = #{
|
|
|
+ get => #{
|
|
|
+ description => <<"Get a bridge by Id">>,
|
|
|
+ parameters => [param_path_id()],
|
|
|
+ responses => #{
|
|
|
+ <<"200">> => emqx_mgmt_util:array_schema(RespSchema,
|
|
|
+ <<"The details of the bridge">>),
|
|
|
+ <<"404">> => emqx_mgmt_util:error_schema(<<"Bridge not found">>, ['NOT_FOUND'])
|
|
|
+ }
|
|
|
+ },
|
|
|
+ put => #{
|
|
|
+ description => <<"Create or update a bridge">>,
|
|
|
+ parameters => [param_path_id()],
|
|
|
+ 'requestBody' => emqx_mgmt_util:schema(ReqSchema),
|
|
|
+ responses => #{
|
|
|
+ <<"200">> => emqx_mgmt_util:array_schema(RespSchema, <<"Bridge updated">>),
|
|
|
+ <<"400">> => emqx_mgmt_util:error_schema(<<"Update bridge failed">>,
|
|
|
+ ['UPDATE_FAILED'])
|
|
|
+ }
|
|
|
+ },
|
|
|
+ delete => #{
|
|
|
+ description => <<"Delete a bridge">>,
|
|
|
+ parameters => [param_path_id()],
|
|
|
+ responses => #{
|
|
|
+ <<"200">> => emqx_mgmt_util:schema(<<"Bridge deleted">>),
|
|
|
+ <<"404">> => emqx_mgmt_util:error_schema(<<"Bridge not found">>, ['NOT_FOUND'])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {"/bridges/:id", Metadata, crud_bridges_cluster}.
|
|
|
+
|
|
|
+operation_apis() ->
|
|
|
+ Metadata = #{
|
|
|
+ post => #{
|
|
|
+ description => <<"Restart bridges on all nodes in the cluster">>,
|
|
|
+ parameters => [
|
|
|
+ param_path_id(),
|
|
|
+ param_path_operation()],
|
|
|
+ responses => #{
|
|
|
+ <<"500">> => emqx_mgmt_util:error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']),
|
|
|
+ <<"200">> => emqx_mgmt_util:schema(<<"Operation success">>)}}},
|
|
|
+ {"/bridges/:id/operation/:operation", Metadata, manage_bridges}.
|
|
|
+
|
|
|
+param_path_id() ->
|
|
|
+ #{
|
|
|
+ name => id,
|
|
|
+ in => path,
|
|
|
+ schema => #{type => string},
|
|
|
+ required => true
|
|
|
+ }.
|
|
|
+
|
|
|
+param_path_operation()->
|
|
|
+ #{
|
|
|
+ name => operation,
|
|
|
+ in => path,
|
|
|
+ required => true,
|
|
|
+ schema => #{
|
|
|
+ type => string,
|
|
|
+ enum => [start, stop, restart]},
|
|
|
+ example => restart
|
|
|
+ }.
|
|
|
+
|
|
|
+list_bridges(get, _Params) ->
|
|
|
+ {200, lists:append([list_local_bridges(Node) || Node <- ekka_mnesia:running_nodes()])}.
|
|
|
+
|
|
|
+list_local_bridges(Node) when Node =:= node() ->
|
|
|
+ [format_resp(Data) || Data <- emqx_bridge:list_bridges()];
|
|
|
+list_local_bridges(Node) ->
|
|
|
+ rpc_call(Node, list_local_bridges, [Node]).
|
|
|
+
|
|
|
+crud_bridges_cluster(Method, Params) ->
|
|
|
+ Results = [crud_bridges(Node, Method, Params) || Node <- ekka_mnesia:running_nodes()],
|
|
|
+ case lists:filter(fun({200}) -> false; ({200, _}) -> false; (_) -> true end, Results) of
|
|
|
+ [] ->
|
|
|
+ case Results of
|
|
|
+ [{200} | _] -> {200};
|
|
|
+ _ -> {200, [Res || {200, Res} <- Results]}
|
|
|
+ end;
|
|
|
+ Errors ->
|
|
|
+ hd(Errors)
|
|
|
end.
|
|
|
|
|
|
-create_bridge(#{name := Name}, Params) ->
|
|
|
- Config = proplists:get_value(<<"config">>, Params),
|
|
|
- BridgeType = proplists:get_value(<<"type">>, Params),
|
|
|
- case emqx_resource:check_and_create(
|
|
|
- emqx_bridge:name_to_resource_id(Name),
|
|
|
- emqx_bridge:resource_type(atom(BridgeType)), maps:from_list(Config)) of
|
|
|
- {ok, already_created} ->
|
|
|
- {400, #{code => 102, message => <<"bridge already created: ", Name/binary>>}};
|
|
|
- {ok, Data} ->
|
|
|
- update_config_and_reply(Name, BridgeType, Config, Data);
|
|
|
- {error, Reason0} ->
|
|
|
- Reason = emqx_resource_api:stringnify(Reason0),
|
|
|
- {500, #{code => 102, message => <<"create bridge ", Name/binary,
|
|
|
- " failed:", Reason/binary>>}}
|
|
|
- end.
|
|
|
+crud_bridges(Node, Method, Params) when Node =/= node() ->
|
|
|
+ rpc_call(Node, crud_bridges, [Node, Method, Params]);
|
|
|
|
|
|
-update_bridge(#{name := Name}, Params) ->
|
|
|
- Config = proplists:get_value(<<"config">>, Params),
|
|
|
- BridgeType = proplists:get_value(<<"type">>, Params),
|
|
|
- case emqx_resource:check_and_update(
|
|
|
- emqx_bridge:name_to_resource_id(Name),
|
|
|
- emqx_bridge:resource_type(atom(BridgeType)), maps:from_list(Config), []) of
|
|
|
- {ok, Data} ->
|
|
|
- update_config_and_reply(Name, BridgeType, Config, Data);
|
|
|
+crud_bridges(_, get, #{bindings := #{id := Id}}) ->
|
|
|
+ ?TRY_PARSE_ID(Id, case emqx_bridge:get_bridge(BridgeType, BridgeName) of
|
|
|
+ {ok, Data} -> {200, format_resp(Data)};
|
|
|
{error, not_found} ->
|
|
|
- {400, #{code => 102, message => <<"bridge not_found: ", Name/binary>>}};
|
|
|
- {error, Reason0} ->
|
|
|
- Reason = emqx_resource_api:stringnify(Reason0),
|
|
|
- {500, #{code => 102, message => <<"update bridge ", Name/binary,
|
|
|
- " failed:", Reason/binary>>}}
|
|
|
- end.
|
|
|
-
|
|
|
-delete_bridge(#{name := Name}, _Params) ->
|
|
|
- case emqx_resource:remove(emqx_bridge:name_to_resource_id(Name)) of
|
|
|
- ok -> delete_config_and_reply(Name);
|
|
|
- {error, Reason} ->
|
|
|
- {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}}
|
|
|
- end.
|
|
|
-
|
|
|
-format_api_reply(#{resource_type := Type, id := Id, config := Conf, status := Status}) ->
|
|
|
- #{type => emqx_bridge:bridge_type(Type),
|
|
|
- name => emqx_bridge:resource_id_to_name(Id),
|
|
|
- config => Conf, status => Status}.
|
|
|
-
|
|
|
-% format_conf(#{resource_type := Type, id := Id, config := Conf}) ->
|
|
|
-% #{type => Type, name => emqx_bridge:resource_id_to_name(Id),
|
|
|
-% config => Conf}.
|
|
|
-
|
|
|
-% get_all_configs() ->
|
|
|
-% [format_conf(Data) || Data <- emqx_bridge:list_bridges()].
|
|
|
-
|
|
|
-update_config_and_reply(Name, BridgeType, Config, Data) ->
|
|
|
- case emqx_bridge:update_config({update, ?BRIDGE(Name, BridgeType, Config)}) of
|
|
|
- {ok, _} ->
|
|
|
- {200, #{code => 0, data => format_api_reply(
|
|
|
- emqx_resource_api:format_data(Data))}};
|
|
|
- {error, Reason} ->
|
|
|
- {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}}
|
|
|
+ {404, #{code => 102, message => <<"not_found: ", Id/binary>>}}
|
|
|
+ end);
|
|
|
+
|
|
|
+crud_bridges(_, put, #{bindings := #{id := Id}, body := Conf}) ->
|
|
|
+ ?TRY_PARSE_ID(Id,
|
|
|
+ case emqx:update_config(emqx_bridge:config_key_path() ++ [BridgeType, BridgeName], Conf,
|
|
|
+ #{rawconf_with_defaults => true}) of
|
|
|
+ {ok, #{raw_config := RawConf, post_config_update := #{emqx_bridge := Data}}} ->
|
|
|
+ {200, format_resp(#{id => Id, raw_config => RawConf, resource_data => Data})};
|
|
|
+ {ok, _} -> %% the bridge already exits
|
|
|
+ {ok, Data} = emqx_bridge:get_bridge(BridgeType, BridgeName),
|
|
|
+ {200, format_resp(Data)};
|
|
|
+ {error, Reason} ->
|
|
|
+ {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}}
|
|
|
+ end);
|
|
|
+
|
|
|
+crud_bridges(_, delete, #{bindings := #{id := Id}}) ->
|
|
|
+ ?TRY_PARSE_ID(Id,
|
|
|
+ case emqx:remove_config(emqx_bridge:config_key_path() ++ [BridgeType, BridgeName]) of
|
|
|
+ {ok, _} -> {200};
|
|
|
+ {error, Reason} ->
|
|
|
+ {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}}
|
|
|
+ end).
|
|
|
+
|
|
|
+format_resp(#{id := Id, raw_config := RawConf, resource_data := #{mod := Mod, status := Status}}) ->
|
|
|
+ IsConnected = fun(started) -> true; (_) -> false end,
|
|
|
+ RawConf#{
|
|
|
+ id => Id,
|
|
|
+ node => node(),
|
|
|
+ bridge_type => emqx_bridge:bridge_type(Mod),
|
|
|
+ is_connected => IsConnected(Status)
|
|
|
+ }.
|
|
|
+
|
|
|
+rpc_call(Node, Fun, Args) ->
|
|
|
+ case rpc:call(Node, ?MODULE, Fun, Args) of
|
|
|
+ {badrpc, Reason} -> {error, Reason};
|
|
|
+ Res -> Res
|
|
|
end.
|
|
|
-
|
|
|
-delete_config_and_reply(Name) ->
|
|
|
- case emqx_bridge:update_config({delete, Name}) of
|
|
|
- {ok, _} -> {200, #{code => 0, data => #{}}};
|
|
|
- {error, Reason} ->
|
|
|
- {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}}
|
|
|
- end.
|
|
|
-
|
|
|
-atom(B) when is_binary(B) ->
|
|
|
- list_to_existing_atom(binary_to_list(B)).
|