feat: add `/sources*` HTTP APIs

This commit is contained in:
Thales Macedo Garitezi 2024-01-08 17:24:55 -03:00
parent e6ccfa5b39
commit 28de7c89c7
11 changed files with 870 additions and 201 deletions

View File

@ -23,6 +23,7 @@
bridge_to_resource_type/1, bridge_to_resource_type/1,
resource_id/1, resource_id/1,
resource_id/2, resource_id/2,
resource_id/3,
bridge_id/2, bridge_id/2,
parse_bridge_id/1, parse_bridge_id/1,
parse_bridge_id/2, parse_bridge_id/2,
@ -62,6 +63,9 @@
?IS_BI_DIR_BRIDGE(TYPE) ?IS_BI_DIR_BRIDGE(TYPE)
). ).
-define(ROOT_KEY_ACTIONS, actions).
-define(ROOT_KEY_SOURCES, sources).
-if(?EMQX_RELEASE_EDITION == ee). -if(?EMQX_RELEASE_EDITION == ee).
bridge_to_resource_type(BridgeType) when is_binary(BridgeType) -> bridge_to_resource_type(BridgeType) when is_binary(BridgeType) ->
bridge_to_resource_type(binary_to_existing_atom(BridgeType, utf8)); bridge_to_resource_type(binary_to_existing_atom(BridgeType, utf8));
@ -85,11 +89,21 @@ bridge_impl_module(_BridgeType) -> undefined.
-endif. -endif.
resource_id(BridgeId) when is_binary(BridgeId) -> resource_id(BridgeId) when is_binary(BridgeId) ->
resource_id_for_kind(?ROOT_KEY_ACTIONS, BridgeId).
resource_id(BridgeType, BridgeName) ->
resource_id(?ROOT_KEY_ACTIONS, BridgeType, BridgeName).
resource_id(ConfRootKey, BridgeType, BridgeName) ->
BridgeId = bridge_id(BridgeType, BridgeName),
resource_id_for_kind(ConfRootKey, BridgeId).
resource_id_for_kind(ConfRootKey, BridgeId) when is_binary(BridgeId) ->
case binary:split(BridgeId, <<":">>) of case binary:split(BridgeId, <<":">>) of
[Type, _Name] -> [Type, _Name] ->
case emqx_bridge_v2:is_bridge_v2_type(Type) of case emqx_bridge_v2:is_bridge_v2_type(Type) of
true -> true ->
emqx_bridge_v2:bridge_v1_id_to_connector_resource_id(BridgeId); emqx_bridge_v2:bridge_v1_id_to_connector_resource_id(ConfRootKey, BridgeId);
false -> false ->
<<"bridge:", BridgeId/binary>> <<"bridge:", BridgeId/binary>>
end; end;
@ -97,10 +111,6 @@ resource_id(BridgeId) when is_binary(BridgeId) ->
invalid_data(<<"should be of pattern {type}:{name}, but got ", BridgeId/binary>>) invalid_data(<<"should be of pattern {type}:{name}, but got ", BridgeId/binary>>)
end. end.
resource_id(BridgeType, BridgeName) ->
BridgeId = bridge_id(BridgeType, BridgeName),
resource_id(BridgeId).
bridge_id(BridgeType, BridgeName) -> bridge_id(BridgeType, BridgeName) ->
Name = bin(BridgeName), Name = bin(BridgeName),
Type = bin(BridgeType), Type = bin(BridgeType),

View File

@ -91,6 +91,7 @@
id/2, id/2,
id/3, id/3,
bridge_v1_is_valid/2, bridge_v1_is_valid/2,
bridge_v1_is_valid/3,
extract_connector_id_from_bridge_v2_id/1 extract_connector_id_from_bridge_v2_id/1
]). ]).
@ -128,6 +129,7 @@
%% Exception from the naming convention: %% Exception from the naming convention:
bridge_v2_type_to_bridge_v1_type/2, bridge_v2_type_to_bridge_v1_type/2,
bridge_v1_id_to_connector_resource_id/1, bridge_v1_id_to_connector_resource_id/1,
bridge_v1_id_to_connector_resource_id/2,
bridge_v1_enable_disable/3, bridge_v1_enable_disable/3,
bridge_v1_restart/2, bridge_v1_restart/2,
bridge_v1_stop/2, bridge_v1_stop/2,
@ -567,7 +569,7 @@ connector_operation_helper(ConfRootKey, BridgeV2Type, Name, ConnectorOpFun, DoHe
ConfRootKey, ConfRootKey,
BridgeV2Type, BridgeV2Type,
Name, Name,
lookup_conf(BridgeV2Type, Name), lookup_conf(ConfRootKey, BridgeV2Type, Name),
ConnectorOpFun, ConnectorOpFun,
DoHealthCheck DoHealthCheck
). ).
@ -1191,8 +1193,11 @@ unpack_bridge_conf(Type, PackedConf, TopLevelConf) ->
%% * The corresponding bridge v2 should exist %% * The corresponding bridge v2 should exist
%% * The connector for the bridge v2 should have exactly one channel %% * The connector for the bridge v2 should have exactly one channel
bridge_v1_is_valid(BridgeV1Type, BridgeName) -> bridge_v1_is_valid(BridgeV1Type, BridgeName) ->
bridge_v1_is_valid(?ROOT_KEY_ACTIONS, BridgeV1Type, BridgeName).
bridge_v1_is_valid(ConfRootKey, BridgeV1Type, BridgeName) ->
BridgeV2Type = ?MODULE:bridge_v1_type_to_bridge_v2_type(BridgeV1Type), BridgeV2Type = ?MODULE:bridge_v1_type_to_bridge_v2_type(BridgeV1Type),
case lookup_conf(BridgeV2Type, BridgeName) of case lookup_conf(ConfRootKey, BridgeV2Type, BridgeName) of
{error, _} -> {error, _} ->
%% If the bridge v2 does not exist, it is a valid bridge v1 %% If the bridge v2 does not exist, it is a valid bridge v1
true; true;
@ -1241,17 +1246,20 @@ bridge_v1_list_and_transform() ->
bridge_v1_lookup_and_transform(ActionType, Name) -> bridge_v1_lookup_and_transform(ActionType, Name) ->
case lookup_actions_or_sources(ActionType, Name) of case lookup_actions_or_sources(ActionType, Name) of
{ok, ConfRootName, {ok, ConfRootKey,
#{raw_config := #{<<"connector">> := ConnectorName} = RawConfig} = ActionConfig} -> #{raw_config := #{<<"connector">> := ConnectorName} = RawConfig} = ActionConfig} ->
BridgeV1Type = ?MODULE:bridge_v2_type_to_bridge_v1_type(ActionType, RawConfig), BridgeV1Type = ?MODULE:bridge_v2_type_to_bridge_v1_type(ActionType, RawConfig),
HasBridgeV1Equivalent = has_bridge_v1_equivalent(ActionType), HasBridgeV1Equivalent = has_bridge_v1_equivalent(ActionType),
case HasBridgeV1Equivalent andalso ?MODULE:bridge_v1_is_valid(BridgeV1Type, Name) of case
HasBridgeV1Equivalent andalso
?MODULE:bridge_v1_is_valid(ConfRootKey, BridgeV1Type, Name)
of
true -> true ->
ConnectorType = connector_type(ActionType), ConnectorType = connector_type(ActionType),
case emqx_connector:lookup(ConnectorType, ConnectorName) of case emqx_connector:lookup(ConnectorType, ConnectorName) of
{ok, Connector} -> {ok, Connector} ->
bridge_v1_lookup_and_transform_helper( bridge_v1_lookup_and_transform_helper(
ConfRootName, ConfRootKey,
BridgeV1Type, BridgeV1Type,
Name, Name,
ActionType, ActionType,
@ -1718,11 +1726,14 @@ connector_has_channels(BridgeV2Type, ConnectorName) ->
end. end.
bridge_v1_id_to_connector_resource_id(BridgeId) -> bridge_v1_id_to_connector_resource_id(BridgeId) ->
bridge_v1_id_to_connector_resource_id(?ROOT_KEY_ACTIONS, BridgeId).
bridge_v1_id_to_connector_resource_id(ConfRootKey, BridgeId) ->
case binary:split(BridgeId, <<":">>) of case binary:split(BridgeId, <<":">>) of
[Type, Name] -> [Type, Name] ->
BridgeV2Type = bin(bridge_v1_type_to_bridge_v2_type(Type)), BridgeV2Type = bin(bridge_v1_type_to_bridge_v2_type(Type)),
ConnectorName = ConnectorName =
case lookup_conf(BridgeV2Type, Name) of case lookup_conf(ConfRootKey, BridgeV2Type, Name) of
#{connector := Con} -> #{connector := Con} ->
Con; Con;
{error, Reason} -> {error, Reason} ->

View File

@ -37,7 +37,7 @@
namespace/0 namespace/0
]). ]).
%% API callbacks %% API callbacks : actions
-export([ -export([
'/actions'/2, '/actions'/2,
'/actions/:id'/2, '/actions/:id'/2,
@ -49,6 +49,18 @@
'/actions_probe'/2, '/actions_probe'/2,
'/action_types'/2 '/action_types'/2
]). ]).
%% API callbacks : sources
-export([
'/sources'/2,
'/sources/:id'/2,
'/sources/:id/metrics'/2,
'/sources/:id/metrics/reset'/2,
'/sources/:id/enable/:enable'/2,
'/sources/:id/:operation'/2,
'/nodes/:node/sources/:id/:operation'/2,
'/sources_probe'/2,
'/source_types'/2
]).
%% BpAPI / RPC Targets %% BpAPI / RPC Targets
-export([ -export([
@ -81,13 +93,16 @@
end end
). ).
namespace() -> "actions". namespace() -> "actions_and_sources".
api_spec() -> api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}). emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
paths() -> paths() ->
[ [
%%=============
%% Actions
%%=============
"/actions", "/actions",
"/actions/:id", "/actions/:id",
"/actions/:id/enable/:enable", "/actions/:id/enable/:enable",
@ -98,7 +113,21 @@ paths() ->
"/actions/:id/metrics", "/actions/:id/metrics",
"/actions/:id/metrics/reset", "/actions/:id/metrics/reset",
"/actions_probe", "/actions_probe",
"/action_types" "/action_types",
%%=============
%% Sources
%%=============
"/sources",
"/sources/:id",
"/sources/:id/enable/:enable",
"/sources/:id/:operation",
"/nodes/:node/sources/:id/:operation",
%% %% Caveat: metrics paths must come *after* `/:operation', otherwise minirest will
%% %% try to match the latter first, trying to interpret `metrics' as an operation...
"/sources/:id/metrics",
"/sources/:id/metrics/reset",
"/sources_probe"
%% "/source_types"
]. ].
error_schema(Code, Message) -> error_schema(Code, Message) ->
@ -111,17 +140,28 @@ error_schema(Codes, Message, ExtraFields) when is_list(Message) ->
error_schema(Codes, Message, ExtraFields) when is_list(Codes) andalso is_binary(Message) -> error_schema(Codes, Message, ExtraFields) when is_list(Codes) andalso is_binary(Message) ->
ExtraFields ++ emqx_dashboard_swagger:error_codes(Codes, Message). ExtraFields ++ emqx_dashboard_swagger:error_codes(Codes, Message).
get_response_body_schema() -> actions_get_response_body_schema() ->
emqx_dashboard_swagger:schema_with_examples( emqx_dashboard_swagger:schema_with_examples(
emqx_bridge_v2_schema:get_response(), emqx_bridge_v2_schema:actions_get_response(),
bridge_info_examples(get) bridge_info_examples(get, ?ROOT_KEY_ACTIONS)
). ).
bridge_info_examples(Method) -> sources_get_response_body_schema() ->
emqx_bridge_v2_schema:examples(Method). emqx_dashboard_swagger:schema_with_examples(
emqx_bridge_v2_schema:sources_get_response(),
bridge_info_examples(get, ?ROOT_KEY_SOURCES)
).
bridge_info_array_example(Method) -> bridge_info_examples(Method, ?ROOT_KEY_ACTIONS) ->
lists:map(fun(#{value := Config}) -> Config end, maps:values(bridge_info_examples(Method))). emqx_bridge_v2_schema:actions_examples(Method);
bridge_info_examples(Method, ?ROOT_KEY_SOURCES) ->
emqx_bridge_v2_schema:sources_examples(Method).
bridge_info_array_example(Method, ConfRootKey) ->
lists:map(
fun(#{value := Config}) -> Config end,
maps:values(bridge_info_examples(Method, ConfRootKey))
).
param_path_id() -> param_path_id() ->
{id, {id,
@ -195,6 +235,9 @@ param_path_enable() ->
} }
)}. )}.
%%================================================================================
%% Actions
%%================================================================================
schema("/actions") -> schema("/actions") ->
#{ #{
'operationId' => '/actions', 'operationId' => '/actions',
@ -204,8 +247,8 @@ schema("/actions") ->
description => ?DESC("desc_api1"), description => ?DESC("desc_api1"),
responses => #{ responses => #{
200 => emqx_dashboard_swagger:schema_with_example( 200 => emqx_dashboard_swagger:schema_with_example(
array(emqx_bridge_v2_schema:get_response()), array(emqx_bridge_v2_schema:actions_get_response()),
bridge_info_array_example(get) bridge_info_array_example(get, ?ROOT_KEY_ACTIONS)
) )
} }
}, },
@ -214,11 +257,11 @@ schema("/actions") ->
summary => <<"Create bridge">>, summary => <<"Create bridge">>,
description => ?DESC("desc_api2"), description => ?DESC("desc_api2"),
'requestBody' => emqx_dashboard_swagger:schema_with_examples( 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
emqx_bridge_v2_schema:post_request(), emqx_bridge_v2_schema:actions_post_request(),
bridge_info_examples(post) bridge_info_examples(post, ?ROOT_KEY_ACTIONS)
), ),
responses => #{ responses => #{
201 => get_response_body_schema(), 201 => actions_get_response_body_schema(),
400 => error_schema('ALREADY_EXISTS', "Bridge already exists") 400 => error_schema('ALREADY_EXISTS', "Bridge already exists")
} }
} }
@ -232,7 +275,7 @@ schema("/actions/:id") ->
description => ?DESC("desc_api3"), description => ?DESC("desc_api3"),
parameters => [param_path_id()], parameters => [param_path_id()],
responses => #{ responses => #{
200 => get_response_body_schema(), 200 => actions_get_response_body_schema(),
404 => error_schema('NOT_FOUND', "Bridge not found") 404 => error_schema('NOT_FOUND', "Bridge not found")
} }
}, },
@ -242,11 +285,11 @@ schema("/actions/:id") ->
description => ?DESC("desc_api4"), description => ?DESC("desc_api4"),
parameters => [param_path_id()], parameters => [param_path_id()],
'requestBody' => emqx_dashboard_swagger:schema_with_examples( 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
emqx_bridge_v2_schema:put_request(), emqx_bridge_v2_schema:actions_put_request(),
bridge_info_examples(put) bridge_info_examples(put, ?ROOT_KEY_ACTIONS)
), ),
responses => #{ responses => #{
200 => get_response_body_schema(), 200 => actions_get_response_body_schema(),
404 => error_schema('NOT_FOUND', "Bridge not found"), 404 => error_schema('NOT_FOUND', "Bridge not found"),
400 => error_schema('BAD_REQUEST', "Update bridge failed") 400 => error_schema('BAD_REQUEST', "Update bridge failed")
} }
@ -371,8 +414,8 @@ schema("/actions_probe") ->
desc => ?DESC("desc_api9"), desc => ?DESC("desc_api9"),
summary => <<"Test creating bridge">>, summary => <<"Test creating bridge">>,
'requestBody' => emqx_dashboard_swagger:schema_with_examples( 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
emqx_bridge_v2_schema:post_request(), emqx_bridge_v2_schema:actions_post_request(),
bridge_info_examples(post) bridge_info_examples(post, ?ROOT_KEY_ACTIONS)
), ),
responses => #{ responses => #{
204 => <<"Test bridge OK">>, 204 => <<"Test bridge OK">>,
@ -389,12 +432,223 @@ schema("/action_types") ->
summary => <<"List available action types">>, summary => <<"List available action types">>,
responses => #{ responses => #{
200 => emqx_dashboard_swagger:schema_with_examples( 200 => emqx_dashboard_swagger:schema_with_examples(
array(emqx_bridge_v2_schema:types_sc()), array(emqx_bridge_v2_schema:action_types_sc()),
#{ #{
<<"types">> => <<"types">> =>
#{ #{
summary => <<"Action types">>, summary => <<"Action types">>,
value => emqx_bridge_v2_schema:types() value => emqx_bridge_v2_schema:action_types()
}
}
)
}
}
};
%%================================================================================
%% Sources
%%================================================================================
schema("/sources") ->
#{
'operationId' => '/sources',
get => #{
tags => [<<"sources">>],
summary => <<"List sources">>,
description => ?DESC("desc_api1"),
responses => #{
%% FIXME: examples
200 => emqx_dashboard_swagger:schema_with_example(
array(emqx_bridge_v2_schema:sources_get_response()),
bridge_info_array_example(get, ?ROOT_KEY_SOURCES)
)
}
},
post => #{
tags => [<<"sources">>],
summary => <<"Create source">>,
description => ?DESC("desc_api2"),
%% FIXME: examples
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
emqx_bridge_v2_schema:sources_post_request(),
bridge_info_examples(post, ?ROOT_KEY_SOURCES)
),
responses => #{
201 => sources_get_response_body_schema(),
400 => error_schema('ALREADY_EXISTS', "Source already exists")
}
}
};
schema("/sources/:id") ->
#{
'operationId' => '/sources/:id',
get => #{
tags => [<<"sources">>],
summary => <<"Get source">>,
description => ?DESC("desc_api3"),
parameters => [param_path_id()],
responses => #{
200 => sources_get_response_body_schema(),
404 => error_schema('NOT_FOUND', "Source not found")
}
},
put => #{
tags => [<<"sources">>],
summary => <<"Update source">>,
description => ?DESC("desc_api4"),
parameters => [param_path_id()],
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
emqx_bridge_v2_schema:sources_put_request(),
bridge_info_examples(put, ?ROOT_KEY_SOURCES)
),
responses => #{
200 => sources_get_response_body_schema(),
404 => error_schema('NOT_FOUND', "Source not found"),
400 => error_schema('BAD_REQUEST', "Update source failed")
}
},
delete => #{
tags => [<<"sources">>],
summary => <<"Delete source">>,
description => ?DESC("desc_api5"),
parameters => [param_path_id(), param_qs_delete_cascade()],
responses => #{
204 => <<"Source deleted">>,
400 => error_schema(
'BAD_REQUEST',
"Cannot delete bridge while active rules are defined for this source",
[{rules, mk(array(string()), #{desc => "Dependent Rule IDs"})}]
),
404 => error_schema('NOT_FOUND', "Source not found"),
503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable")
}
}
};
schema("/sources/:id/metrics") ->
#{
'operationId' => '/sources/:id/metrics',
get => #{
tags => [<<"sources">>],
summary => <<"Get source metrics">>,
description => ?DESC("desc_bridge_metrics"),
parameters => [param_path_id()],
responses => #{
200 => emqx_bridge_schema:metrics_fields(),
404 => error_schema('NOT_FOUND', "Source not found")
}
}
};
schema("/sources/:id/metrics/reset") ->
#{
'operationId' => '/sources/:id/metrics/reset',
put => #{
tags => [<<"sources">>],
summary => <<"Reset source metrics">>,
description => ?DESC("desc_api6"),
parameters => [param_path_id()],
responses => #{
204 => <<"Reset success">>,
404 => error_schema('NOT_FOUND', "Source not found")
}
}
};
schema("/sources/:id/enable/:enable") ->
#{
'operationId' => '/sources/:id/enable/:enable',
put =>
#{
tags => [<<"sources">>],
summary => <<"Enable or disable bridge">>,
desc => ?DESC("desc_enable_bridge"),
parameters => [param_path_id(), param_path_enable()],
responses =>
#{
204 => <<"Success">>,
404 => error_schema(
'NOT_FOUND', "Bridge not found or invalid operation"
),
503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable")
}
}
};
schema("/sources/:id/:operation") ->
#{
'operationId' => '/sources/:id/:operation',
post => #{
tags => [<<"sources">>],
summary => <<"Manually start a bridge">>,
description => ?DESC("desc_api7"),
parameters => [
param_path_id(),
param_path_operation_cluster()
],
responses => #{
204 => <<"Operation success">>,
400 => error_schema(
'BAD_REQUEST', "Problem with configuration of external service"
),
404 => error_schema('NOT_FOUND', "Bridge not found or invalid operation"),
501 => error_schema('NOT_IMPLEMENTED', "Not Implemented"),
503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable")
}
}
};
schema("/nodes/:node/sources/:id/:operation") ->
#{
'operationId' => '/nodes/:node/sources/:id/:operation',
post => #{
tags => [<<"sources">>],
summary => <<"Manually start a bridge on a given node">>,
description => ?DESC("desc_api8"),
parameters => [
param_path_node(),
param_path_id(),
param_path_operation_on_node()
],
responses => #{
204 => <<"Operation success">>,
400 => error_schema(
'BAD_REQUEST',
"Problem with configuration of external service or bridge not enabled"
),
404 => error_schema(
'NOT_FOUND', "Bridge or node not found or invalid operation"
),
501 => error_schema('NOT_IMPLEMENTED', "Not Implemented"),
503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable")
}
}
};
schema("/sources_probe") ->
#{
'operationId' => '/sources_probe',
post => #{
tags => [<<"sources">>],
desc => ?DESC("desc_api9"),
summary => <<"Test creating bridge">>,
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
emqx_bridge_v2_schema:sources_post_request(),
bridge_info_examples(post, ?ROOT_KEY_SOURCES)
),
responses => #{
204 => <<"Test bridge OK">>,
400 => error_schema(['TEST_FAILED'], "bridge test failed")
}
}
};
schema("/source_types") ->
#{
'operationId' => '/source_types',
get => #{
tags => [<<"sources">>],
desc => ?DESC("desc_api10"),
summary => <<"List available source types">>,
responses => #{
200 => emqx_dashboard_swagger:schema_with_examples(
array(emqx_bridge_v2_schema:action_types_sc()),
#{
<<"types">> =>
#{
summary => <<"Source types">>,
value => emqx_bridge_v2_schema:action_types()
} }
} }
) )
@ -402,6 +656,12 @@ schema("/action_types") ->
} }
}. }.
%%------------------------------------------------------------------------------
%% Thin Handlers
%%------------------------------------------------------------------------------
%%================================================================================
%% Actions
%%================================================================================
'/actions'(post, #{body := #{<<"type">> := BridgeType, <<"name">> := BridgeName} = Conf0}) -> '/actions'(post, #{body := #{<<"type">> := BridgeType, <<"name">> := BridgeName} = Conf0}) ->
handle_create(?ROOT_KEY_ACTIONS, BridgeType, BridgeName, Conf0); handle_create(?ROOT_KEY_ACTIONS, BridgeType, BridgeName, Conf0);
'/actions'(get, _Params) -> '/actions'(get, _Params) ->
@ -439,7 +699,48 @@ schema("/action_types") ->
handle_probe(?ROOT_KEY_ACTIONS, Request). handle_probe(?ROOT_KEY_ACTIONS, Request).
'/action_types'(get, _Request) -> '/action_types'(get, _Request) ->
?OK(emqx_bridge_v2_schema:types()). ?OK(emqx_bridge_v2_schema:action_types()).
%%================================================================================
%% Sources
%%================================================================================
'/sources'(post, #{body := #{<<"type">> := BridgeType, <<"name">> := BridgeName} = Conf0}) ->
handle_create(?ROOT_KEY_SOURCES, BridgeType, BridgeName, Conf0);
'/sources'(get, _Params) ->
handle_list(?ROOT_KEY_SOURCES).
'/sources/:id'(get, #{bindings := #{id := Id}}) ->
?TRY_PARSE_ID(Id, lookup_from_all_nodes(?ROOT_KEY_SOURCES, BridgeType, BridgeName, 200));
'/sources/:id'(put, #{bindings := #{id := Id}, body := Conf0}) ->
handle_update(?ROOT_KEY_SOURCES, Id, Conf0);
'/sources/:id'(delete, #{bindings := #{id := Id}, query_string := Qs}) ->
handle_delete(?ROOT_KEY_SOURCES, Id, Qs).
'/sources/:id/metrics'(get, #{bindings := #{id := Id}}) ->
?TRY_PARSE_ID(Id, get_metrics_from_all_nodes(?ROOT_KEY_SOURCES, BridgeType, BridgeName)).
'/sources/:id/metrics/reset'(put, #{bindings := #{id := Id}}) ->
handle_reset_metrics(?ROOT_KEY_SOURCES, Id).
'/sources/:id/enable/:enable'(put, #{bindings := #{id := Id, enable := Enable}}) ->
handle_disable_enable(?ROOT_KEY_SOURCES, Id, Enable).
'/sources/:id/:operation'(post, #{
bindings :=
#{id := Id, operation := Op}
}) ->
handle_operation(?ROOT_KEY_SOURCES, Id, Op).
'/nodes/:node/sources/:id/:operation'(post, #{
bindings :=
#{id := Id, operation := Op, node := Node}
}) ->
handle_node_operation(?ROOT_KEY_SOURCES, Node, Id, Op).
'/sources_probe'(post, Request) ->
handle_probe(?ROOT_KEY_SOURCES, Request).
'/source_types'(get, _Request) ->
?OK(emqx_bridge_v2_schema:source_types()).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Handlers %% Handlers
@ -451,7 +752,7 @@ handle_list(ConfRootKey) ->
case is_ok(NodeReplies) of case is_ok(NodeReplies) of
{ok, NodeBridges} -> {ok, NodeBridges} ->
AllBridges = [ AllBridges = [
[format_resource(Data, Node) || Data <- Bridges] [format_resource(ConfRootKey, Data, Node) || Data <- Bridges]
|| {Node, Bridges} <- lists:zip(Nodes, NodeBridges) || {Node, Bridges} <- lists:zip(Nodes, NodeBridges)
], ],
?OK(zip_bridges(AllBridges)); ?OK(zip_bridges(AllBridges));
@ -574,7 +875,12 @@ handle_node_operation(ConfRootKey, Node, Id, Op) ->
). ).
handle_probe(ConfRootKey, Request) -> handle_probe(ConfRootKey, Request) ->
RequestMeta = #{module => ?MODULE, method => post, path => "/actions_probe"}, Path =
case ConfRootKey of
?ROOT_KEY_ACTIONS -> "/actions_probe";
?ROOT_KEY_SOURCES -> "/sources_probe"
end,
RequestMeta = #{module => ?MODULE, method => post, path => Path},
case emqx_dashboard_swagger:filter_check_request_and_translate_body(Request, RequestMeta) of case emqx_dashboard_swagger:filter_check_request_and_translate_body(Request, RequestMeta) of
{ok, #{body := #{<<"type">> := Type} = Params}} -> {ok, #{body := #{<<"type">> := Type} = Params}} ->
Params1 = maybe_deobfuscate_bridge_probe(Params), Params1 = maybe_deobfuscate_bridge_probe(Params),
@ -664,8 +970,8 @@ get_metrics_from_all_nodes(ConfRootKey, Type, Name) ->
?INTERNAL_ERROR(Reason) ?INTERNAL_ERROR(Reason)
end. end.
operation_func(all, start) -> v2_start_bridge_to_all_nodes_v6; operation_func(all, start) -> v2_start_bridge_on_all_nodes_v6;
operation_func(_Node, start) -> v2_start_bridge_to_node_v6; operation_func(_Node, start) -> v2_start_bridge_on_node_v6;
operation_func(all, lookup) -> v2_lookup_from_all_nodes_v6; operation_func(all, lookup) -> v2_lookup_from_all_nodes_v6;
operation_func(all, list) -> v2_list_bridges_on_nodes_v6; operation_func(all, list) -> v2_list_bridges_on_nodes_v6;
operation_func(all, get_metrics) -> v2_get_metrics_from_all_nodes_v6. operation_func(all, get_metrics) -> v2_get_metrics_from_all_nodes_v6.
@ -825,7 +1131,7 @@ aggregate_status(AllStatus) ->
%% RPC Target %% RPC Target
lookup_from_local_node(BridgeType, BridgeName) -> lookup_from_local_node(BridgeType, BridgeName) ->
case emqx_bridge_v2:lookup(BridgeType, BridgeName) of case emqx_bridge_v2:lookup(BridgeType, BridgeName) of
{ok, Res} -> {ok, format_resource(Res, node())}; {ok, Res} -> {ok, format_resource(?ROOT_KEY_ACTIONS, Res, node())};
Error -> Error Error -> Error
end. end.
@ -833,7 +1139,7 @@ lookup_from_local_node(BridgeType, BridgeName) ->
-spec lookup_from_local_node_v6(emqx_bridge_v2:root_cfg_key(), _, _) -> _. -spec lookup_from_local_node_v6(emqx_bridge_v2:root_cfg_key(), _, _) -> _.
lookup_from_local_node_v6(ConfRootKey, BridgeType, BridgeName) -> lookup_from_local_node_v6(ConfRootKey, BridgeType, BridgeName) ->
case emqx_bridge_v2:lookup(ConfRootKey, BridgeType, BridgeName) of case emqx_bridge_v2:lookup(ConfRootKey, BridgeType, BridgeName) of
{ok, Res} -> {ok, format_resource(Res, node())}; {ok, Res} -> {ok, format_resource(ConfRootKey, Res, node())};
Error -> Error Error -> Error
end. end.
@ -847,6 +1153,7 @@ get_metrics_from_local_node_v6(ConfRootKey, Type, Name) ->
%% resource %% resource
format_resource( format_resource(
ConfRootKey,
#{ #{
type := Type, type := Type,
name := Name, name := Name,
@ -857,7 +1164,7 @@ format_resource(
}, },
Node Node
) -> ) ->
RawConf = fill_defaults(Type, RawConf0), RawConf = fill_defaults(ConfRootKey, Type, RawConf0),
redact( redact(
maps:merge( maps:merge(
RawConf#{ RawConf#{
@ -988,17 +1295,18 @@ aggregate_metrics(
M17 + N17 M17 + N17
). ).
fill_defaults(Type, RawConf) -> fill_defaults(ConfRootKey, Type, RawConf) ->
PackedConf = pack_bridge_conf(Type, RawConf), PackedConf = pack_bridge_conf(ConfRootKey, Type, RawConf),
FullConf = emqx_config:fill_defaults(emqx_bridge_v2_schema, PackedConf, #{}), FullConf = emqx_config:fill_defaults(emqx_bridge_v2_schema, PackedConf, #{}),
unpack_bridge_conf(Type, FullConf). unpack_bridge_conf(ConfRootKey, Type, FullConf).
pack_bridge_conf(Type, RawConf) -> pack_bridge_conf(ConfRootKey, Type, RawConf) ->
#{<<"actions">> => #{bin(Type) => #{<<"foo">> => RawConf}}}. #{bin(ConfRootKey) => #{bin(Type) => #{<<"foo">> => RawConf}}}.
unpack_bridge_conf(Type, PackedConf) -> unpack_bridge_conf(ConfRootKey, Type, PackedConf) ->
ConfRootKeyBin = bin(ConfRootKey),
TypeBin = bin(Type), TypeBin = bin(Type),
#{<<"actions">> := Bridges} = PackedConf, #{ConfRootKeyBin := Bridges} = PackedConf,
#{<<"foo">> := RawConf} = maps:get(TypeBin, Bridges), #{<<"foo">> := RawConf} = maps:get(TypeBin, Bridges),
RawConf. RawConf.

View File

@ -35,8 +35,8 @@
v2_lookup_from_all_nodes_v6/4, v2_lookup_from_all_nodes_v6/4,
v2_list_bridges_on_nodes_v6/2, v2_list_bridges_on_nodes_v6/2,
v2_get_metrics_from_all_nodes_v6/4, v2_get_metrics_from_all_nodes_v6/4,
v2_start_bridge_to_node_v6/4, v2_start_bridge_on_node_v6/4,
v2_start_bridge_to_all_nodes_v6/4 v2_start_bridge_on_all_nodes_v6/4
]). ]).
-include_lib("emqx/include/bpapi.hrl"). -include_lib("emqx/include/bpapi.hrl").
@ -173,9 +173,9 @@ v2_get_metrics_from_all_nodes_v6(Nodes, ConfRootKey, ActionType, ActionName) ->
?TIMEOUT ?TIMEOUT
). ).
-spec v2_start_bridge_to_all_nodes_v6([node()], emqx_bridge_v2:root_cfg_key(), key(), key()) -> -spec v2_start_bridge_on_all_nodes_v6([node()], emqx_bridge_v2:root_cfg_key(), key(), key()) ->
emqx_rpc:erpc_multicall(ok). emqx_rpc:erpc_multicall(ok).
v2_start_bridge_to_all_nodes_v6(Nodes, ConfRootKey, BridgeType, BridgeName) -> v2_start_bridge_on_all_nodes_v6(Nodes, ConfRootKey, BridgeType, BridgeName) ->
erpc:multicall( erpc:multicall(
Nodes, Nodes,
emqx_bridge_v2, emqx_bridge_v2,
@ -184,9 +184,9 @@ v2_start_bridge_to_all_nodes_v6(Nodes, ConfRootKey, BridgeType, BridgeName) ->
?TIMEOUT ?TIMEOUT
). ).
-spec v2_start_bridge_to_node_v6(node(), emqx_bridge_v2:root_cfg_key(), key(), key()) -> -spec v2_start_bridge_on_node_v6(node(), emqx_bridge_v2:root_cfg_key(), key(), key()) ->
term(). term().
v2_start_bridge_to_node_v6(Node, ConfRootKey, BridgeType, BridgeName) -> v2_start_bridge_on_node_v6(Node, ConfRootKey, BridgeType, BridgeName) ->
rpc:call( rpc:call(
Node, Node,
emqx_bridge_v2, emqx_bridge_v2,

View File

@ -28,21 +28,31 @@
-export([roots/0, fields/1, desc/1, namespace/0, tags/0]). -export([roots/0, fields/1, desc/1, namespace/0, tags/0]).
-export([ -export([
get_response/0, actions_get_response/0,
put_request/0, actions_put_request/0,
post_request/0, actions_post_request/0,
examples/1, actions_examples/1,
action_values/4 action_values/4
]). ]).
-export([
sources_get_response/0,
sources_put_request/0,
sources_post_request/0,
sources_examples/1,
source_values/4
]).
%% Exported for mocking %% Exported for mocking
%% TODO: refactor emqx_bridge_v1_compatibility_layer_SUITE so we don't need to %% TODO: refactor emqx_bridge_v1_compatibility_layer_SUITE so we don't need to
%% export this %% export this
-export([ -export([
registered_api_schemas/1 registered_actions_api_schemas/1,
registered_sources_api_schemas/1
]). ]).
-export([types/0, types_sc/0]). -export([action_types/0, action_types_sc/0]).
-export([source_types/0, source_types_sc/0]).
-export([resource_opts_fields/0, resource_opts_fields/1]). -export([resource_opts_fields/0, resource_opts_fields/1]).
-export([ -export([
@ -58,33 +68,140 @@
-export([actions_convert_from_connectors/1]). -export([actions_convert_from_connectors/1]).
-export_type([action_type/0]). -export_type([action_type/0, source_type/0]).
%% Should we explicitly list them here so dialyzer may be more helpful? %% Should we explicitly list them here so dialyzer may be more helpful?
-type action_type() :: atom(). -type action_type() :: atom().
-type source_type() :: atom().
-type http_method() :: get | post | put.
-type schema_example_map() :: #{atom() => term()}.
%%====================================================================================== %%======================================================================================
%% For HTTP APIs %% For HTTP APIs
get_response() -> %%======================================================================================
api_schema("get").
put_request() -> %%---------------------------------------------
api_schema("put"). %% Actions
%%---------------------------------------------
post_request() -> actions_get_response() ->
api_schema("post"). actions_api_schema("get").
api_schema(Method) -> actions_put_request() ->
APISchemas = ?MODULE:registered_api_schemas(Method), actions_api_schema("put").
actions_post_request() ->
actions_api_schema("post").
actions_api_schema(Method) ->
APISchemas = ?MODULE:registered_actions_api_schemas(Method),
hoconsc:union(bridge_api_union(APISchemas)). hoconsc:union(bridge_api_union(APISchemas)).
registered_api_schemas(Method) -> registered_actions_api_schemas(Method) ->
RegisteredSchemas = emqx_action_info:registered_schema_modules_actions(), RegisteredSchemas = emqx_action_info:registered_schema_modules_actions(),
[ [
api_ref(SchemaModule, atom_to_binary(BridgeV2Type), Method ++ "_bridge_v2") api_ref(SchemaModule, atom_to_binary(BridgeV2Type), Method ++ "_bridge_v2")
|| {BridgeV2Type, SchemaModule} <- RegisteredSchemas || {BridgeV2Type, SchemaModule} <- RegisteredSchemas
]. ].
-spec action_values(http_method(), atom(), atom(), schema_example_map()) -> schema_example_map().
action_values(Method, ActionType, ConnectorType, ActionValues) ->
ActionTypeBin = atom_to_binary(ActionType),
ConnectorTypeBin = atom_to_binary(ConnectorType),
lists:foldl(
fun(M1, M2) ->
maps:merge(M1, M2)
end,
#{
enable => true,
description => <<"My example ", ActionTypeBin/binary, " action">>,
connector => <<ConnectorTypeBin/binary, "_connector">>,
resource_opts => #{
health_check_interval => "30s"
}
},
[
ActionValues,
method_values(action, Method, ActionType)
]
).
actions_examples(Method) ->
MergeFun =
fun(Example, Examples) ->
maps:merge(Examples, Example)
end,
Fun =
fun(Module, Examples) ->
ConnectorExamples = erlang:apply(Module, bridge_v2_examples, [Method]),
lists:foldl(MergeFun, Examples, ConnectorExamples)
end,
SchemaModules = [Mod || {_, Mod} <- emqx_action_info:registered_schema_modules_actions()],
lists:foldl(Fun, #{}, SchemaModules).
%%---------------------------------------------
%% Sources
%%---------------------------------------------
sources_get_response() ->
sources_api_schema("get").
sources_put_request() ->
sources_api_schema("put").
sources_post_request() ->
sources_api_schema("post").
sources_api_schema(Method) ->
APISchemas = ?MODULE:registered_sources_api_schemas(Method),
hoconsc:union(bridge_api_union(APISchemas)).
registered_sources_api_schemas(Method) ->
RegisteredSchemas = emqx_action_info:registered_schema_modules_sources(),
[
api_ref(SchemaModule, atom_to_binary(BridgeV2Type), Method ++ "_source")
|| {BridgeV2Type, SchemaModule} <- RegisteredSchemas
].
-spec source_values(http_method(), atom(), atom(), schema_example_map()) -> schema_example_map().
source_values(Method, SourceType, ConnectorType, SourceValues) ->
SourceTypeBin = atom_to_binary(SourceType),
ConnectorTypeBin = atom_to_binary(ConnectorType),
lists:foldl(
fun(M1, M2) ->
maps:merge(M1, M2)
end,
#{
enable => true,
description => <<"My example ", SourceTypeBin/binary, " source">>,
connector => <<ConnectorTypeBin/binary, "_connector">>,
resource_opts => #{
health_check_interval => "30s"
}
},
[
SourceValues,
method_values(source, Method, SourceType)
]
).
sources_examples(Method) ->
MergeFun =
fun(Example, Examples) ->
maps:merge(Examples, Example)
end,
Fun =
fun(Module, Examples) ->
ConnectorExamples = erlang:apply(Module, bridge_v2_examples, [Method]),
lists:foldl(MergeFun, Examples, ConnectorExamples)
end,
SchemaModules = [Mod || {_, Mod} <- emqx_action_info:registered_schema_modules_sources()],
lists:foldl(Fun, #{}, SchemaModules).
%%---------------------------------------------
%% Common helpers
%%---------------------------------------------
api_ref(Module, Type, Method) -> api_ref(Module, Type, Method) ->
{Type, ref(Module, Method)}. {Type, ref(Module, Method)}.
@ -111,41 +228,17 @@ bridge_api_union(Refs) ->
end end
end. end.
-type http_method() :: get | post | put. -spec method_values(action | source, http_method(), atom()) -> schema_example_map().
-type schema_example_map() :: #{atom() => term()}. method_values(Kind, post, Type) ->
KindBin = atom_to_binary(Kind),
-spec action_values(http_method(), atom(), atom(), schema_example_map()) -> schema_example_map().
action_values(Method, ActionType, ConnectorType, ActionValues) ->
ActionTypeBin = atom_to_binary(ActionType),
ConnectorTypeBin = atom_to_binary(ConnectorType),
lists:foldl(
fun(M1, M2) ->
maps:merge(M1, M2)
end,
#{
enable => true,
description => <<"My example ", ActionTypeBin/binary, " action">>,
connector => <<ConnectorTypeBin/binary, "_connector">>,
resource_opts => #{
health_check_interval => "30s"
}
},
[
ActionValues,
method_values(Method, ActionType)
]
).
-spec method_values(http_method(), atom()) -> schema_example_map().
method_values(post, Type) ->
TypeBin = atom_to_binary(Type), TypeBin = atom_to_binary(Type),
#{ #{
name => <<TypeBin/binary, "_action">>, name => <<TypeBin/binary, "_", KindBin/binary>>,
type => TypeBin type => TypeBin
}; };
method_values(get, Type) -> method_values(Kind, get, Type) ->
maps:merge( maps:merge(
method_values(post, Type), method_values(Kind, post, Type),
#{ #{
status => <<"connected">>, status => <<"connected">>,
node_status => [ node_status => [
@ -156,7 +249,7 @@ method_values(get, Type) ->
] ]
} }
); );
method_values(put, _Type) -> method_values(_Kind, put, _Type) ->
#{}. #{}.
api_fields("get_bridge_v2", Type, Fields) -> api_fields("get_bridge_v2", Type, Fields) ->
@ -175,16 +268,33 @@ api_fields("post_bridge_v2", Type, Fields) ->
] ]
); );
api_fields("put_bridge_v2", _Type, Fields) -> api_fields("put_bridge_v2", _Type, Fields) ->
Fields;
api_fields("get_source", Type, Fields) ->
lists:append(
[
emqx_bridge_schema:type_and_name_fields(Type),
emqx_bridge_schema:status_fields(),
Fields
]
);
api_fields("post_source", Type, Fields) ->
lists:append(
[
emqx_bridge_schema:type_and_name_fields(Type),
Fields
]
);
api_fields("put_source", _Type, Fields) ->
Fields. Fields.
%%====================================================================================== %%======================================================================================
%% HOCON Schema Callbacks %% HOCON Schema Callbacks
%%====================================================================================== %%======================================================================================
namespace() -> "actions". namespace() -> "actions_and_sources".
tags() -> tags() ->
[<<"Actions">>]. [<<"Actions">>, <<"Sources">>].
-dialyzer({nowarn_function, roots/0}). -dialyzer({nowarn_function, roots/0}).
@ -231,13 +341,21 @@ desc(resource_opts) ->
desc(_) -> desc(_) ->
undefined. undefined.
-spec types() -> [action_type()]. -spec action_types() -> [action_type()].
types() -> action_types() ->
proplists:get_keys(?MODULE:fields(actions)). proplists:get_keys(?MODULE:fields(actions)).
-spec types_sc() -> ?ENUM([action_type()]). -spec action_types_sc() -> ?ENUM([action_type()]).
types_sc() -> action_types_sc() ->
hoconsc:enum(types()). hoconsc:enum(action_types()).
-spec source_types() -> [source_type()].
source_types() ->
proplists:get_keys(?MODULE:fields(sources)).
-spec source_types_sc() -> ?ENUM([source_type()]).
source_types_sc() ->
hoconsc:enum(source_types()).
resource_opts_fields() -> resource_opts_fields() ->
resource_opts_fields(_Overrides = []). resource_opts_fields(_Overrides = []).
@ -268,19 +386,6 @@ resource_opts_fields(Overrides) ->
emqx_resource_schema:create_opts(Overrides) emqx_resource_schema:create_opts(Overrides)
). ).
examples(Method) ->
MergeFun =
fun(Example, Examples) ->
maps:merge(Examples, Example)
end,
Fun =
fun(Module, Examples) ->
ConnectorExamples = erlang:apply(Module, bridge_v2_examples, [Method]),
lists:foldl(MergeFun, Examples, ConnectorExamples)
end,
SchemaModules = [Mod || {_, Mod} <- emqx_action_info:registered_schema_modules_actions()],
lists:foldl(Fun, #{}, SchemaModules).
top_level_common_action_keys() -> top_level_common_action_keys() ->
[ [
<<"connector">>, <<"connector">>,

View File

@ -104,7 +104,7 @@ setup_mocks() ->
catch meck:new(emqx_bridge_v2_schema, MeckOpts), catch meck:new(emqx_bridge_v2_schema, MeckOpts),
meck:expect( meck:expect(
emqx_bridge_v2_schema, emqx_bridge_v2_schema,
registered_api_schemas, registered_actions_api_schemas,
1, 1,
fun(Method) -> fun(Method) ->
[{bridge_type_bin(), hoconsc:ref(?MODULE, "api_v2_" ++ Method)}] [{bridge_type_bin(), hoconsc:ref(?MODULE, "api_v2_" ++ Method)}]

View File

@ -12,6 +12,9 @@
-import(emqx_common_test_helpers, [on_exit/1]). -import(emqx_common_test_helpers, [on_exit/1]).
-define(ROOT_KEY_ACTIONS, actions).
-define(ROOT_KEY_SOURCES, sources).
%% ct setup helpers %% ct setup helpers
init_per_suite(Config, Apps) -> init_per_suite(Config, Apps) ->
@ -152,6 +155,49 @@ create_bridge(Config, Overrides) ->
ct:pal("creating bridge with config: ~p", [BridgeConfig]), ct:pal("creating bridge with config: ~p", [BridgeConfig]),
emqx_bridge_v2:create(BridgeType, BridgeName, BridgeConfig). emqx_bridge_v2:create(BridgeType, BridgeName, BridgeConfig).
get_ct_config_with_fallback(Config, [Key]) ->
?config(Key, Config);
get_ct_config_with_fallback(Config, [Key | Rest]) ->
case ?config(Key, Config) of
undefined ->
get_ct_config_with_fallback(Config, Rest);
X ->
X
end.
get_config_by_kind(Config, Overrides) ->
Kind = ?config(bridge_kind, Config),
get_config_by_kind(Kind, Config, Overrides).
get_config_by_kind(Kind, Config, Overrides) ->
case Kind of
action ->
%% TODO: refactor tests to use action_type...
ActionType = get_ct_config_with_fallback(Config, [action_type, bridge_type]),
ActionName = get_ct_config_with_fallback(Config, [action_name, bridge_name]),
ActionConfig0 = get_ct_config_with_fallback(Config, [action_config, bridge_config]),
ActionConfig = emqx_utils_maps:deep_merge(ActionConfig0, Overrides),
#{type => ActionType, name => ActionName, config => ActionConfig};
source ->
SourceType = ?config(source_type, Config),
SourceName = ?config(source_name, Config),
SourceConfig0 = ?config(source_config, Config),
SourceConfig = emqx_utils_maps:deep_merge(SourceConfig0, Overrides),
#{type => SourceType, name => SourceName, config => SourceConfig}
end.
api_path_root(Kind) ->
case Kind of
action -> "actions";
source -> "sources"
end.
conf_root_key(Kind) ->
case Kind of
action -> ?ROOT_KEY_ACTIONS;
source -> ?ROOT_KEY_SOURCES
end.
maybe_json_decode(X) -> maybe_json_decode(X) ->
case emqx_utils_json:safe_decode(X, [return_maps]) of case emqx_utils_json:safe_decode(X, [return_maps]) of
{ok, Decoded} -> Decoded; {ok, Decoded} -> Decoded;
@ -218,26 +264,26 @@ create_bridge_api(Config) ->
create_bridge_api(Config, _Overrides = #{}). create_bridge_api(Config, _Overrides = #{}).
create_bridge_api(Config, Overrides) -> create_bridge_api(Config, Overrides) ->
BridgeType = ?config(bridge_type, Config),
BridgeName = ?config(bridge_name, Config),
BridgeConfig0 = ?config(bridge_config, Config),
BridgeConfig = emqx_utils_maps:deep_merge(BridgeConfig0, Overrides),
{ok, {{_, 201, _}, _, _}} = create_connector_api(Config), {ok, {{_, 201, _}, _, _}} = create_connector_api(Config),
create_kind_api(Config, Overrides).
Params = BridgeConfig#{<<"type">> => BridgeType, <<"name">> => BridgeName}, create_kind_api(Config) ->
Path = emqx_mgmt_api_test_util:api_path(["actions"]), create_kind_api(Config, _Overrides = #{}).
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
Opts = #{return_all => true}, create_kind_api(Config, Overrides) ->
ct:pal("creating bridge (via http): ~p", [Params]), Kind = proplists:get_value(bridge_kind, Config, action),
Res = #{
case emqx_mgmt_api_test_util:request_api(post, Path, "", AuthHeader, Params, Opts) of type := Type,
{ok, {Status, Headers, Body0}} -> name := Name,
{ok, {Status, Headers, emqx_utils_json:decode(Body0, [return_maps])}}; config := BridgeConfig
Error -> } = get_config_by_kind(Kind, Config, Overrides),
Error Params = BridgeConfig#{<<"type">> => Type, <<"name">> => Name},
end, PathRoot = api_path_root(Kind),
ct:pal("bridge create result: ~p", [Res]), Path = emqx_mgmt_api_test_util:api_path([PathRoot]),
ct:pal("creating bridge (~s, http):\n ~p", [Kind, Params]),
Method = post,
Res = request(Method, Path, Params),
ct:pal("bridge create (~s, http) result:\n ~p", [Kind, Res]),
Res. Res.
create_connector_api(Config) -> create_connector_api(Config) ->
@ -288,27 +334,29 @@ update_bridge_api(Config) ->
update_bridge_api(Config, _Overrides = #{}). update_bridge_api(Config, _Overrides = #{}).
update_bridge_api(Config, Overrides) -> update_bridge_api(Config, Overrides) ->
BridgeType = ?config(bridge_type, Config), Kind = proplists:get_value(bridge_kind, Config, action),
Name = ?config(bridge_name, Config), #{
BridgeConfig0 = ?config(bridge_config, Config), type := Type,
BridgeConfig = emqx_utils_maps:deep_merge(BridgeConfig0, Overrides), name := Name,
BridgeId = emqx_bridge_resource:bridge_id(BridgeType, Name), config := Params
Path = emqx_mgmt_api_test_util:api_path(["actions", BridgeId]), } = get_config_by_kind(Kind, Config, Overrides),
AuthHeader = emqx_mgmt_api_test_util:auth_header_(), BridgeId = emqx_bridge_resource:bridge_id(Type, Name),
Opts = #{return_all => true}, PathRoot = api_path_root(Kind),
ct:pal("updating bridge (via http): ~p", [BridgeConfig]), Path = emqx_mgmt_api_test_util:api_path([PathRoot, BridgeId]),
Res = ct:pal("updating bridge (~s, http):\n ~p", [Kind, Params]),
case emqx_mgmt_api_test_util:request_api(put, Path, "", AuthHeader, BridgeConfig, Opts) of Method = put,
{ok, {_Status, _Headers, Body0}} -> {ok, emqx_utils_json:decode(Body0, [return_maps])}; Res = request(Method, Path, Params),
Error -> Error ct:pal("update bridge (~s, http) result:\n ~p", [Kind, Res]),
end,
ct:pal("bridge update result: ~p", [Res]),
Res. Res.
op_bridge_api(Op, BridgeType, BridgeName) -> op_bridge_api(Op, BridgeType, BridgeName) ->
op_bridge_api(_Kind = action, Op, BridgeType, BridgeName).
op_bridge_api(Kind, Op, BridgeType, BridgeName) ->
BridgeId = emqx_bridge_resource:bridge_id(BridgeType, BridgeName), BridgeId = emqx_bridge_resource:bridge_id(BridgeType, BridgeName),
Path = emqx_mgmt_api_test_util:api_path(["actions", BridgeId, Op]), PathRoot = api_path_root(Kind),
ct:pal("calling bridge ~p (via http): ~p", [BridgeId, Op]), Path = emqx_mgmt_api_test_util:api_path([PathRoot, BridgeId, Op]),
ct:pal("calling bridge ~p (~s, http):\n ~p", [BridgeId, Kind, Op]),
Method = post, Method = post,
Params = [], Params = [],
Res = request(Method, Path, Params), Res = request(Method, Path, Params),
@ -326,17 +374,16 @@ probe_bridge_api(Config, Overrides) ->
probe_bridge_api(BridgeType, BridgeName, BridgeConfig). probe_bridge_api(BridgeType, BridgeName, BridgeConfig).
probe_bridge_api(BridgeType, BridgeName, BridgeConfig) -> probe_bridge_api(BridgeType, BridgeName, BridgeConfig) ->
probe_bridge_api(action, BridgeType, BridgeName, BridgeConfig).
probe_bridge_api(Kind, BridgeType, BridgeName, BridgeConfig) ->
Params = BridgeConfig#{<<"type">> => BridgeType, <<"name">> => BridgeName}, Params = BridgeConfig#{<<"type">> => BridgeType, <<"name">> => BridgeName},
Path = emqx_mgmt_api_test_util:api_path(["actions_probe"]), PathRoot = api_path_root(Kind),
AuthHeader = emqx_mgmt_api_test_util:auth_header_(), Path = emqx_mgmt_api_test_util:api_path([PathRoot ++ "_probe"]),
Opts = #{return_all => true}, ct:pal("probing bridge (~s, http):\n ~p", [Kind, Params]),
ct:pal("probing bridge (via http): ~p", [Params]), Method = post,
Res = Res = request(Method, Path, Params),
case emqx_mgmt_api_test_util:request_api(post, Path, "", AuthHeader, Params, Opts) of ct:pal("bridge probe (~s, http) result:\n ~p", [Kind, Res]),
{ok, {{_, 204, _}, _Headers, _Body0} = Res0} -> {ok, Res0};
Error -> Error
end,
ct:pal("bridge probe result: ~p", [Res]),
Res. Res.
list_bridges_http_api_v1() -> list_bridges_http_api_v1() ->
@ -353,6 +400,13 @@ list_actions_http_api() ->
ct:pal("list actions (http v2) result:\n ~p", [Res]), ct:pal("list actions (http v2) result:\n ~p", [Res]),
Res. Res.
list_sources_http_api() ->
Path = emqx_mgmt_api_test_util:api_path(["sources"]),
ct:pal("list sources (http v2)"),
Res = request(get, Path, _Params = []),
ct:pal("list sources (http v2) result:\n ~p", [Res]),
Res.
list_connectors_http_api() -> list_connectors_http_api() ->
Path = emqx_mgmt_api_test_util:api_path(["connectors"]), Path = emqx_mgmt_api_test_util:api_path(["connectors"]),
ct:pal("list connectors"), ct:pal("list connectors"),
@ -506,13 +560,6 @@ t_create_via_http(Config) ->
begin begin
?assertMatch({ok, _}, create_bridge_api(Config)), ?assertMatch({ok, _}, create_bridge_api(Config)),
%% lightweight matrix testing some configs
?assertMatch(
{ok, _},
update_bridge_api(
Config
)
),
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
update_bridge_api( update_bridge_api(
@ -526,23 +573,26 @@ t_create_via_http(Config) ->
ok. ok.
t_start_stop(Config, StopTracePoint) -> t_start_stop(Config, StopTracePoint) ->
BridgeType = ?config(bridge_type, Config), Kind = proplists:get_value(bridge_kind, Config, action),
BridgeName = ?config(bridge_name, Config),
BridgeConfig = ?config(bridge_config, Config),
ConnectorName = ?config(connector_name, Config), ConnectorName = ?config(connector_name, Config),
ConnectorType = ?config(connector_type, Config), ConnectorType = ?config(connector_type, Config),
ConnectorConfig = ?config(connector_config, Config), #{
type := Type,
name := Name,
config := BridgeConfig
} = get_config_by_kind(Kind, Config, _Overrides = #{}),
?assertMatch( ?assertMatch(
{ok, _}, {ok, {{_, 201, _}, _, _}},
emqx_connector:create(ConnectorType, ConnectorName, ConnectorConfig) create_connector_api(Config)
), ),
?check_trace( ?check_trace(
begin begin
ProbeRes0 = probe_bridge_api( ProbeRes0 = probe_bridge_api(
BridgeType, Kind,
BridgeName, Type,
Name,
BridgeConfig BridgeConfig
), ),
?assertMatch({ok, {{_, 204, _}, _Headers, _Body}}, ProbeRes0), ?assertMatch({ok, {{_, 204, _}, _Headers, _Body}}, ProbeRes0),
@ -550,8 +600,9 @@ t_start_stop(Config, StopTracePoint) ->
AtomsBefore = erlang:system_info(atom_count), AtomsBefore = erlang:system_info(atom_count),
%% Probe again; shouldn't have created more atoms. %% Probe again; shouldn't have created more atoms.
ProbeRes1 = probe_bridge_api( ProbeRes1 = probe_bridge_api(
BridgeType, Kind,
BridgeName, Type,
Name,
BridgeConfig BridgeConfig
), ),
@ -559,9 +610,9 @@ t_start_stop(Config, StopTracePoint) ->
AtomsAfter = erlang:system_info(atom_count), AtomsAfter = erlang:system_info(atom_count),
?assertEqual(AtomsBefore, AtomsAfter), ?assertEqual(AtomsBefore, AtomsAfter),
?assertMatch({ok, _}, emqx_bridge_v2:create(BridgeType, BridgeName, BridgeConfig)), ?assertMatch({ok, _}, create_kind_api(Config)),
ResourceId = emqx_bridge_resource:resource_id(BridgeType, BridgeName), ResourceId = emqx_bridge_resource:resource_id(conf_root_key(Kind), Type, Name),
%% Since the connection process is async, we give it some time to %% Since the connection process is async, we give it some time to
%% stabilize and avoid flakiness. %% stabilize and avoid flakiness.
@ -574,7 +625,7 @@ t_start_stop(Config, StopTracePoint) ->
%% `start` bridge to trigger `already_started` %% `start` bridge to trigger `already_started`
?assertMatch( ?assertMatch(
{ok, {{_, 204, _}, _Headers, []}}, {ok, {{_, 204, _}, _Headers, []}},
emqx_bridge_v2_testlib:op_bridge_api("start", BridgeType, BridgeName) op_bridge_api(Kind, "start", Type, Name)
), ),
?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceId)), ?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceId)),
@ -624,10 +675,10 @@ t_start_stop(Config, StopTracePoint) ->
) )
), ),
ok #{resource_id => ResourceId}
end, end,
fun(Trace) -> fun(Res, Trace) ->
ResourceId = emqx_bridge_resource:resource_id(BridgeType, BridgeName), #{resource_id := ResourceId} = Res,
%% one for each probe, one for real %% one for each probe, one for real
?assertMatch( ?assertMatch(
[_, _, #{instance_id := ResourceId}], [_, _, #{instance_id := ResourceId}],

View File

@ -108,7 +108,7 @@ connector_resource_opts_test() ->
ok. ok.
actions_api_spec_post_fields_test() -> actions_api_spec_post_fields_test() ->
?UNION(Union) = emqx_bridge_v2_schema:post_request(), ?UNION(Union) = emqx_bridge_v2_schema:actions_post_request(),
Schemas = Schemas =
lists:map( lists:map(
fun(?R_REF(SchemaMod, StructName)) -> fun(?R_REF(SchemaMod, StructName)) ->

View File

@ -237,7 +237,9 @@ on_stop(ResourceId, State) ->
ets:delete(TopicToHandlerIndex) ets:delete(TopicToHandlerIndex)
end, end,
Allocated = emqx_resource:get_allocated_resources(ResourceId), Allocated = emqx_resource:get_allocated_resources(ResourceId),
ok = stop_helper(Allocated). ok = stop_helper(Allocated),
?tp(mqtt_connector_stopped, #{instance_id => ResourceId}),
ok.
stop_helper(#{pool_name := PoolName}) -> stop_helper(#{pool_name := PoolName}) ->
emqx_resource_pool:stop(PoolName). emqx_resource_pool:stop(PoolName).

View File

@ -27,6 +27,9 @@
conn_bridge_examples/1 conn_bridge_examples/1
]). ]).
-define(ACTION_TYPE, mqtt).
-define(SOURCE_TYPE, mqtt).
%%====================================================================================== %%======================================================================================
%% Hocon Schema Definitions %% Hocon Schema Definitions
namespace() -> "bridge_mqtt_publisher". namespace() -> "bridge_mqtt_publisher".
@ -86,14 +89,18 @@ fields(action_resource_opts) ->
fun({K, _V}) -> not lists:member(K, UnsupportedOpts) end, fun({K, _V}) -> not lists:member(K, UnsupportedOpts) end,
emqx_bridge_v2_schema:resource_opts_fields() emqx_bridge_v2_schema:resource_opts_fields()
); );
fields("get_connector") -> fields(Field) when
emqx_bridge_mqtt_connector_schema:fields("config_connector"); Field == "get_bridge_v2";
fields("get_bridge_v2") -> Field == "post_bridge_v2";
fields("mqtt_publisher_action"); Field == "put_bridge_v2"
fields("post_bridge_v2") -> ->
fields("mqtt_publisher_action") ++ emqx_bridge_schema:type_and_name_fields(mqtt); emqx_bridge_v2_schema:api_fields(Field, ?ACTION_TYPE, fields("mqtt_publisher_action"));
fields("put_bridge_v2") -> fields(Field) when
fields("mqtt_publisher_action"); Field == "get_source";
Field == "post_source";
Field == "put_source"
->
emqx_bridge_v2_schema:api_fields(Field, ?SOURCE_TYPE, fields("mqtt_subscriber_source"));
fields(What) -> fields(What) ->
error({emqx_bridge_mqtt_pubsub_schema, missing_field_handler, What}). error({emqx_bridge_mqtt_pubsub_schema, missing_field_handler, What}).
%% v2: api schema %% v2: api schema

View File

@ -0,0 +1,175 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_bridge_mqtt_v2_subscriber_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_hooks.hrl").
-include_lib("stdlib/include/assert.hrl").
-include_lib("emqx/include/asserts.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-import(emqx_common_test_helpers, [on_exit/1]).
%%------------------------------------------------------------------------------
%% CT boilerplate
%%------------------------------------------------------------------------------
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
Apps = emqx_cth_suite:start(
[
emqx,
emqx_conf,
emqx_connector,
emqx_bridge_mqtt,
emqx_bridge,
emqx_rule_engine,
emqx_management,
{emqx_dashboard, "dashboard.listeners.http { enable = true, bind = 18083 }"}
],
#{work_dir => emqx_cth_suite:work_dir(Config)}
),
{ok, Api} = emqx_common_test_http:create_default_app(),
[
{apps, Apps},
{api, Api}
| Config
].
end_per_suite(Config) ->
Apps = ?config(apps, Config),
emqx_cth_suite:stop(Apps),
ok.
init_per_testcase(TestCase, Config) ->
UniqueNum = integer_to_binary(erlang:unique_integer()),
Name = iolist_to_binary([atom_to_binary(TestCase), UniqueNum]),
ConnectorConfig = connector_config(),
SourceConfig = source_config(#{connector => Name}),
[
{bridge_kind, source},
{source_type, mqtt},
{source_name, Name},
{source_config, SourceConfig},
{connector_type, mqtt},
{connector_name, Name},
{connector_config, ConnectorConfig}
| Config
].
%%------------------------------------------------------------------------------
%% Helper fns
%%------------------------------------------------------------------------------
connector_config() ->
%% !!!!!!!!!!!! FIXME!!!!!! add more fields ("server_configs")
#{
<<"enable">> => true,
<<"description">> => <<"my connector">>,
<<"pool_size">> => 3,
<<"server">> => <<"127.0.0.1:1883">>,
<<"resource_opts">> => #{
<<"health_check_interval">> => <<"15s">>,
<<"start_after_created">> => true,
<<"start_timeout">> => <<"5s">>
}
}.
source_config(Overrides0) ->
Overrides = emqx_utils_maps:binary_key_map(Overrides0),
CommonConfig =
#{
<<"enable">> => true,
<<"connector">> => <<"please override">>,
<<"parameters">> =>
#{
<<"remote">> =>
#{
<<"topic">> => <<"remote/topic">>,
<<"qos">> => 2
},
<<"local">> =>
#{
<<"topic">> => <<"local/topic">>,
<<"qos">> => 2,
<<"retain">> => false,
<<"payload">> => <<"${payload}">>
}
},
<<"resource_opts">> => #{
<<"batch_size">> => 1,
<<"batch_time">> => <<"0ms">>,
<<"buffer_mode">> => <<"memory_only">>,
<<"buffer_seg_bytes">> => <<"10MB">>,
<<"health_check_interval">> => <<"15s">>,
<<"inflight_window">> => 100,
<<"max_buffer_bytes">> => <<"256MB">>,
<<"metrics_flush_interval">> => <<"1s">>,
<<"query_mode">> => <<"sync">>,
<<"request_ttl">> => <<"45s">>,
<<"resume_interval">> => <<"15s">>,
<<"worker_pool_size">> => <<"1">>
}
},
maps:merge(CommonConfig, Overrides).
replace(Key, Value, Proplist) ->
lists:keyreplace(Key, 1, Proplist, {Key, Value}).
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_create_via_http(Config) ->
ConnectorName = ?config(connector_name, Config),
ok = emqx_bridge_v2_testlib:t_create_via_http(Config),
?assertMatch(
{ok,
{{_, 200, _}, _, [
#{
<<"enable">> := true,
<<"status">> := <<"connected">>
}
]}},
emqx_bridge_v2_testlib:list_bridges_http_api_v1()
),
NewSourceName = <<"my_other_source">>,
{ok, {{_, 201, _}, _, _}} =
emqx_bridge_v2_testlib:create_kind_api(
replace(source_name, NewSourceName, Config)
),
?assertMatch(
{ok,
{{_, 200, _}, _, [
#{<<"connector">> := ConnectorName},
#{<<"connector">> := ConnectorName}
]}},
emqx_bridge_v2_testlib:list_sources_http_api()
),
?assertMatch(
{ok, {{_, 200, _}, _, []}},
emqx_bridge_v2_testlib:list_bridges_http_api_v1()
),
ok.
t_start_stop(Config) ->
ok = emqx_bridge_v2_testlib:t_start_stop(Config, mqtt_connector_stopped),
ok.