diff --git a/apps/emqx/src/emqx_ctl.erl b/apps/emqx/src/emqx_ctl.erl index 3ca8174fe..42baf9854 100644 --- a/apps/emqx/src/emqx_ctl.erl +++ b/apps/emqx/src/emqx_ctl.erl @@ -160,18 +160,28 @@ format(Format, Args) -> -spec(format_usage([cmd_usage()]) -> [string()]). format_usage(UsageList) -> + Width = lists:foldl(fun({CmdStr, _}, W) -> + max(iolist_size(CmdStr), W) + end, 0, UsageList), lists:map( fun({CmdParams, Desc}) -> - format_usage(CmdParams, Desc) + format_usage(CmdParams, Desc, Width) end, UsageList). -spec(format_usage(cmd_params(), cmd_descr()) -> string()). format_usage(CmdParams, Desc) -> + format_usage(CmdParams, Desc, 0). + +format_usage(CmdParams, Desc, 0) -> + format_usage(CmdParams, Desc, iolist_size(CmdParams)); +format_usage(CmdParams, Desc, Width) -> CmdLines = split_cmd(CmdParams), DescLines = split_cmd(Desc), + Zipped = zip_cmd(CmdLines, DescLines), + Fmt = "~-" ++ integer_to_list(Width + 1) ++ "s# ~ts~n", lists:foldl(fun({CmdStr, DescStr}, Usage) -> - Usage ++ format("~-70s# ~ts~n", [CmdStr, DescStr]) - end, "", zip_cmd(CmdLines, DescLines)). + Usage ++ format(Fmt, [CmdStr, DescStr]) + end, "", Zipped). %%-------------------------------------------------------------------- %% gen_server callbacks diff --git a/apps/emqx/src/emqx_logger_jsonfmt.erl b/apps/emqx/src/emqx_logger_jsonfmt.erl index 387c9b4c3..32fd418f9 100644 --- a/apps/emqx/src/emqx_logger_jsonfmt.erl +++ b/apps/emqx/src/emqx_logger_jsonfmt.erl @@ -31,6 +31,9 @@ -export([format/2]). +%% For CLI outputs +-export([best_effort_json/1]). + -ifdef(TEST). -include_lib("proper/include/proper.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -51,6 +54,16 @@ -define(IS_STRING(String), (is_list(String) orelse is_binary(String))). +%% @doc Format a list() or map() to JSON object. +%% This is used for CLI result prints, +%% or HTTP API result formatting. +%% The JSON object is pretty-printed. +%% NOTE: do not use this function for logging. +best_effort_json(Input) -> + Config = #{depth => unlimited, single_line => true}, + JsonReady = best_effort_json_obj(Input, Config), + jsx:encode(JsonReady, [space, {indent, 4}]). + -spec format(logger:log_event(), config()) -> iodata(). format(#{level := Level, msg := Msg, meta := Meta}, Config0) when is_map(Config0) -> Config = add_default_config(Config0), diff --git a/apps/emqx/test/emqx_run_sh.erl b/apps/emqx/test/emqx_run_sh.erl new file mode 100644 index 000000000..ede301410 --- /dev/null +++ b/apps/emqx/test/emqx_run_sh.erl @@ -0,0 +1,43 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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_run_sh). +-export([do/2]). + +do(Command, Options0) -> + Options = Options0 ++ [use_stdio, stderr_to_stdout, + exit_status, {line, 906}, hide, eof], + Port = erlang:open_port({spawn, Command}, Options), + try + collect_output(Port, []) + after + erlang:port_close(Port) + end. + +collect_output(Port, Lines) -> + receive + {Port, {data, {eol, Line}}} -> + collect_output(Port, [Line ++ "\n" | Lines]); + {Port, {data, {noeol, Line}}} -> + collect_output(Port, [Line | Lines]); + {Port, eof} -> + Result = lists:flatten(lists:reverse(Lines)), + receive + {Port, {exit_status, 0}} -> + {ok, Result}; + {Port, {exit_status, ExitCode}} -> + {error, {ExitCode, Result}} + end + end. diff --git a/apps/emqx_machine/src/emqx_machine_boot.erl b/apps/emqx_machine/src/emqx_machine_boot.erl index 9fab5854e..24541990b 100644 --- a/apps/emqx_machine/src/emqx_machine_boot.erl +++ b/apps/emqx_machine/src/emqx_machine_boot.erl @@ -28,7 +28,6 @@ post_boot() -> ok = ensure_apps_started(), - _ = emqx_plugins:load(), ok = print_vsn(), ok = start_autocluster(), ignore. @@ -79,7 +78,7 @@ start_one_app(App) -> end. %% list of app names which should be rebooted when: -%% 1. due to static static config change +%% 1. due to static config change %% 2. after join a cluster reboot_apps() -> [ gproc @@ -104,6 +103,7 @@ reboot_apps() -> , emqx_exhook , emqx_authn , emqx_authz + , emqx_plugins ]. sorted_reboot_apps() -> diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index d45b5ad77..f40ab7b8b 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -79,14 +79,6 @@ , do_unsubscribe/2 ]). -%% Plugins --export([ list_plugins/0 - , list_plugins/1 - , load_plugin/2 - , unload_plugin/2 - , reload_plugin/2 - ]). - %% Listeners -export([ list_listeners/0 , list_listeners/1 @@ -457,33 +449,6 @@ do_unsubscribe(ClientId, Topic) -> Pid ! {unsubscribe, [emqx_topic:parse(Topic)]} end. -%%-------------------------------------------------------------------- -%% Plugins -%%-------------------------------------------------------------------- - -list_plugins() -> - [{Node, list_plugins(Node)} || Node <- mria_mnesia:running_nodes()]. - -list_plugins(Node) when Node =:= node() -> - emqx_plugins:list(); -list_plugins(Node) -> - rpc_call(Node, list_plugins, [Node]). - -load_plugin(Node, Plugin) when Node =:= node() -> - emqx_plugins:load(Plugin); -load_plugin(Node, Plugin) -> - rpc_call(Node, load_plugin, [Node, Plugin]). - -unload_plugin(Node, Plugin) when Node =:= node() -> - emqx_plugins:unload(Plugin); -unload_plugin(Node, Plugin) -> - rpc_call(Node, unload_plugin, [Node, Plugin]). - -reload_plugin(Node, Plugin) when Node =:= node() -> - emqx_plugins:reload(Plugin); -reload_plugin(Node, Plugin) -> - rpc_call(Node, reload_plugin, [Node, Plugin]). - %%-------------------------------------------------------------------- %% Listeners %%-------------------------------------------------------------------- diff --git a/apps/emqx_management/src/emqx_mgmt_cli.erl b/apps/emqx_management/src/emqx_mgmt_cli.erl index 6e082e4ca..5885a2b17 100644 --- a/apps/emqx_management/src/emqx_mgmt_cli.erl +++ b/apps/emqx_management/src/emqx_mgmt_cli.erl @@ -225,47 +225,51 @@ if_valid_qos(QoS, Fun) -> end. plugins(["list"]) -> - lists:foreach(fun print/1, emqx_plugins:list()); - -plugins(["load", Name]) -> - case emqx_plugins:load(list_to_atom(Name)) of - ok -> - emqx_ctl:print("Plugin ~ts loaded successfully.~n", [Name]); - {error, Reason} -> - emqx_ctl:print("Load plugin ~ts error: ~p.~n", [Name, Reason]) - end; - -plugins(["unload", "emqx_management"])-> - emqx_ctl:print("Plugin emqx_management can not be unloaded.~n"); - -plugins(["unload", Name]) -> - case emqx_plugins:unload(list_to_atom(Name)) of - ok -> - emqx_ctl:print("Plugin ~ts unloaded successfully.~n", [Name]); - {error, Reason} -> - emqx_ctl:print("Unload plugin ~ts error: ~p.~n", [Name, Reason]) - end; - -plugins(["reload", Name]) -> - try list_to_existing_atom(Name) of - PluginName -> - case emqx_mgmt:reload_plugin(node(), PluginName) of - ok -> - emqx_ctl:print("Plugin ~ts reloaded successfully.~n", [Name]); - {error, Reason} -> - emqx_ctl:print("Reload plugin ~ts error: ~p.~n", [Name, Reason]) - end - catch - error:badarg -> - emqx_ctl:print("Reload plugin ~ts error: The plugin doesn't exist.~n", [Name]) - end; - + emqx_plugins_cli:list(fun emqx_ctl:print/2); +plugins(["describe", NameVsn]) -> + emqx_plugins_cli:describe(NameVsn, fun emqx_ctl:print/2); +plugins(["install", NameVsn]) -> + emqx_plugins_cli:ensure_installed(NameVsn, fun emqx_ctl:print/2); +plugins(["uninstall", NameVsn])-> + emqx_plugins_cli:ensure_uninstalled(NameVsn, fun emqx_ctl:print/2); +plugins(["start", NameVsn]) -> + emqx_plugins_cli:ensure_started(NameVsn, fun emqx_ctl:print/2); +plugins(["stop", NameVsn]) -> + emqx_plugins_cli:ensure_stopped(NameVsn, fun emqx_ctl:print/2); +plugins(["restart", NameVsn]) -> + emqx_plugins_cli:restart(NameVsn, fun emqx_ctl:print/2); +plugins(["disable", NameVsn]) -> + emqx_plugins_cli:ensure_disabled(NameVsn, fun emqx_ctl:print/2); +plugins(["enable", NameVsn]) -> + emqx_plugins_cli:ensure_enabled(NameVsn, no_move, fun emqx_ctl:print/2); +plugins(["enable", NameVsn, "front"]) -> + emqx_plugins_cli:ensure_enabled(NameVsn, front, fun emqx_ctl:print/2); +plugins(["enable", NameVsn, "rear"]) -> + emqx_plugins_cli:ensure_enabled(NameVsn, rear, fun emqx_ctl:print/2); +plugins(["enable", NameVsn, "before", Other]) -> + emqx_plugins_cli:ensure_enabled(NameVsn, {before, Other}, fun emqx_ctl:print/2); plugins(_) -> - emqx_ctl:usage([{"plugins list", "Show loaded plugins"}, - {"plugins load ", "Load plugin"}, - {"plugins unload ", "Unload plugin"}, - {"plugins reload ", "Reload plugin"} - ]). + emqx_ctl:usage( + [{"plugins [Name-Vsn]", "e.g. 'start emqx_plugin_template-5.0-rc.1'"}, + {"plugins list", "List all installed plugins"}, + {"plugins describe Name-Vsn", "Describe an installed plugins"}, + {"plugins install Name-Vsn", "Install a plugin package placed\n" + "in plugin'sinstall_dir"}, + {"plugins uninstall Name-Vsn", "Uninstall a plugin. NOTE: it deletes\n" + "all files in install_dir/Name-Vsn"}, + {"plugins start Name-Vsn", "Start a plugin"}, + {"plugins stop Name-Vsn", "Stop a plugin"}, + {"plugins restart Name-Vsn", "Stop then start a plugin"}, + {"plugins disable Name-Vsn", "Disable auto-boot"}, + {"plugins enable Name-Vsn [Position]", + "Enable auto-boot at Position in the boot list, where Position could be\n" + "'front', 'rear', or 'before Other-Vsn' to specify a relative position.\n" + "The Position parameter can be used to adjust the boot order.\n" + "If no Position is given, an already configured plugin\n" + "will stary at is old position; a newly plugin is appended to the rear\n" + "e.g. plugins disable foo-0.1.0 front\n" + " plugins enable bar-0.2.0 before foo-0.1.0"} + ]). %%-------------------------------------------------------------------- %% @doc vm command diff --git a/apps/emqx_plugins/include/emqx_plugins.hrl b/apps/emqx_plugins/include/emqx_plugins.hrl new file mode 100644 index 000000000..caf9ac839 --- /dev/null +++ b/apps/emqx_plugins/include/emqx_plugins.hrl @@ -0,0 +1,22 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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. +%%-------------------------------------------------------------------- + +-ifndef(EMQX_PLUGINS_HRL). +-define(EMQX_PLUGINS_HRL, true). + +-define(CONF_ROOT, plugins). + +-endif. diff --git a/apps/emqx_plugins/src/emqx_plugins.erl b/apps/emqx_plugins/src/emqx_plugins.erl index 003ca7ec3..686ea3d75 100644 --- a/apps/emqx_plugins/src/emqx_plugins.erl +++ b/apps/emqx_plugins/src/emqx_plugins.erl @@ -19,13 +19,29 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). --export([ load/0 - , load/1 - , unload/0 - , unload/1 - , reload/1 +-export([ ensure_installed/1 + , ensure_uninstalled/1 + , ensure_enabled/1 + , ensure_enabled/2 + , ensure_disabled/1 + , delete_package/1 + ]). + +-export([ ensure_started/0 + , ensure_started/1 + , ensure_stopped/0 + , ensure_stopped/1 + , restart/1 , list/0 - , find_plugin/1 + , describe/1 + ]). + +-export([ get_config/2 + , put_config/2 + ]). + +%% internal +-export([ do_ensure_started/1 ]). -ifdef(TEST). @@ -33,128 +49,389 @@ -compile(nowarn_export_all). -endif. +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/logger.hrl"). +-include("emqx_plugins.hrl"). + +-type name_vsn() :: binary() | string(). %% "my_plugin-0.1.0" +-type plugin() :: map(). %% the parse result of the JSON info file +-type position() :: no_move | front | rear | {before, name_vsn()}. + %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- -%% @doc Load all plugins when the broker started. --spec(load() -> ok | ignore | {error, term()}). -load() -> - ok = load_ext_plugins(emqx:get_config([plugins, install_dir], undefined)). +%% @doc Describe a plugin. +-spec describe(name_vsn()) -> {ok, plugin()} | {error, any()}. +describe(NameVsn) -> read_plugin(NameVsn). -%% @doc Load a Plugin --spec(load(atom()) -> ok | {error, term()}). -load(PluginName) when is_atom(PluginName) -> - case {lists:member(PluginName, names(plugin)), lists:member(PluginName, names(started_app))} of - {false, _} -> - ?SLOG(alert, #{msg => "failed_to_load_plugin", - plugin_name => PluginName, - reason => not_found}), - {error, not_found}; - {_, true} -> - ?SLOG(notice, #{msg => "plugin_already_loaded", - plugin_name => PluginName, - reason => already_loaded}), - {error, already_started}; - {_, false} -> - load_plugin(PluginName) +%% @doc Install a .tar.gz package placed in install_dir. +-spec ensure_installed(name_vsn()) -> ok | {error, any()}. +ensure_installed(NameVsn) -> + case read_plugin(NameVsn) of + {ok, _} -> + ok; + {error, _} -> + ok = purge(NameVsn), + do_ensure_installed(NameVsn) end. -%% @doc Unload all plugins before broker stopped. --spec(unload() -> ok). -unload() -> - stop_plugins(list()). - -%% @doc UnLoad a Plugin --spec(unload(atom()) -> ok | {error, term()}). -unload(PluginName) when is_atom(PluginName) -> - case {lists:member(PluginName, names(plugin)), lists:member(PluginName, names(started_app))} of - {false, _} -> - ?SLOG(error, #{msg => "fialed_to_unload_plugin", - plugin_name => PluginName, - reason => not_found}), - {error, not_found}; - {_, false} -> - ?SLOG(error, #{msg => "failed_to_unload_plugin", - plugin_name => PluginName, - reason => not_loaded}), - {error, not_started}; - {_, _} -> - unload_plugin(PluginName) +do_ensure_installed(NameVsn) -> + TarGz = pkg_file(NameVsn), + case erl_tar:extract(TarGz, [{cwd, install_dir()}, compressed]) of + ok -> + case read_plugin(NameVsn) of + {ok, _} -> ok; + {error, Reason} -> + ?SLOG(warning, Reason#{msg => "failed_to_read_after_install"}), + _ = ensure_uninstalled(NameVsn), + {error, Reason} + end; + {error, {_, enoent}} -> + {error, #{ reason => "failed_to_extract_plugin_package" + , path => TarGz + , return => not_found + }}; + {error, Reason} -> + {error, #{ reason => "bad_plugin_package" + , path => TarGz + , return => Reason + }} end. -reload(PluginName) when is_atom(PluginName)-> - case {lists:member(PluginName, names(plugin)), lists:member(PluginName, names(started_app))} of - {false, _} -> - ?SLOG(error, #{msg => "failed_to_reload_plugin", - plugin_name => PluginName, - reason => not_found}), - {error, not_found}; - {_, false} -> - load(PluginName); - {_, true} -> - case unload(PluginName) of - ok -> load(PluginName); - {error, Reason} -> {error, Reason} +%% @doc Ensure files and directories for the given plugin are delete. +%% If a plugin is running, or enabled, error is returned. +-spec ensure_uninstalled(name_vsn()) -> ok | {error, any()}. +ensure_uninstalled(NameVsn) -> + case read_plugin(NameVsn) of + {ok, #{running_status := RunningSt}} when RunningSt =/= stopped -> + {error, #{reason => "bad_plugin_running_status", + hint => "stop_the_plugin_first" + }}; + {ok, #{config_status := enabled}} -> + {error, #{reason => "bad_plugin_config_status", + hint => "disable_the_plugin_first" + }}; + _ -> + purge(NameVsn) + end. + +%% @doc Ensure a plugin is enabled to the end of the plugins list. +-spec ensure_enabled(name_vsn()) -> ok | {error, any()}. +ensure_enabled(NameVsn) -> + ensure_enabled(NameVsn, no_move). + +%% @doc Ensure a plugin is enabled at the given position of the plugin list. +-spec ensure_enabled(name_vsn(), position()) -> ok | {error, any()}. +ensure_enabled(NameVsn, Position) -> + ensure_state(NameVsn, Position, true). + +%% @doc Ensure a plugin is disabled. +-spec ensure_disabled(name_vsn()) -> ok | {error, any()}. +ensure_disabled(NameVsn) -> + ensure_state(NameVsn, no_move, false). + +ensure_state(NameVsn, Position, State) when is_binary(NameVsn) -> + ensure_state(binary_to_list(NameVsn), Position, State); +ensure_state(NameVsn, Position, State) -> + case read_plugin(NameVsn) of + {ok, _} -> + Item = #{ name_vsn => NameVsn + , enable => State + }, + tryit("ensure_state", fun() -> ensure_configured(Item, Position) end); + {error, Reason} -> + {error, Reason} + end. + +ensure_configured(#{name_vsn := NameVsn} = Item, Position) -> + Configured = configured(), + SplitFun = fun(#{name_vsn := Nv}) -> bin(Nv) =/= bin(NameVsn) end, + {Front, Rear} = lists:splitwith(SplitFun, Configured), + NewConfigured = + case Rear of + [_ | More] when Position =:= no_move -> + Front ++ [Item | More]; + [_ | More] -> + add_new_configured(Front ++ More, Position, Item); + [] -> + add_new_configured(Configured, Position, Item) + end, + ok = put_configured(NewConfigured). + +add_new_configured(Configured, no_move, Item) -> + %% default to rear + add_new_configured(Configured, rear, Item); +add_new_configured(Configured, front, Item) -> + [Item | Configured]; +add_new_configured(Configured, rear, Item) -> + Configured ++ [Item]; +add_new_configured(Configured, {before, NameVsn}, Item) -> + SplitFun = fun(#{name_vsn := Nv}) -> bin(Nv) =/= bin(NameVsn) end, + {Front, Rear} = lists:splitwith(SplitFun, Configured), + Rear =:= [] andalso + throw(#{error => "position_anchor_plugin_not_configured", + hint => "maybe_install_and_configure", + name_vsn => NameVsn + }), + Front ++ [Item | Rear]. + +%% @doc Delete the package file. +-spec delete_package(name_vsn()) -> ok. +delete_package(NameVsn) -> + File = pkg_file(NameVsn), + case file:delete(File) of + ok -> + ?SLOG(info, #{msg => "purged_plugin_dir", path => File}), + ok; + {error, enoent} -> + ok; + {error, Reason} -> + ?SLOG(error, #{msg => "failed_to_delete_package_file", + path => File, + reason => Reason}), + {error, Reason} + end. + +%% @doc Delete extracted dir +%% In case one lib is shared by multiple plugins. +%% it might be the case that purging one plugin's install dir +%% will cause deletion of loaded beams. +%% It should not be a problem, because shared lib should +%% reside in all the plugin install dirs. +-spec purge(name_vsn()) -> ok. +purge(NameVsn) -> + Dir = dir(NameVsn), + case file:del_dir_r(Dir) of + ok -> + ?SLOG(info, #{msg => "purged_plugin_dir", dir => Dir}); + {error, enoent} -> + ok; + {error, Reason} -> + ?SLOG(error, #{msg => "failed_to_purge_plugin_dir", + dir => Dir, + reason => Reason}), + {error, Reason} + end. + +%% @doc Start all configured plugins are started. +-spec ensure_started() -> ok. +ensure_started() -> + ok = for_plugins(fun ?MODULE:do_ensure_started/1). + +%% @doc Start a plugin from Management API or CLI. +%% the input is a - string. +-spec ensure_started(name_vsn()) -> ok | {error, term()}. +ensure_started(NameVsn) -> + case do_ensure_started(NameVsn) of + ok -> ok; + {error, Reason} -> + ?SLOG(alert, #{msg => "failed_to_start_plugin", + reason => Reason}), + {error, Reason} + end. + +%% @doc Stop all plugins before broker stops. +-spec ensure_stopped() -> ok. +ensure_stopped() -> + for_plugins(fun ?MODULE:ensure_stopped/1). + +%% @doc Stop a plugin from Management API or CLI. +-spec ensure_stopped(name_vsn()) -> ok | {error, term()}. +ensure_stopped(NameVsn) -> + tryit("stop_plugin", + fun() -> + Plugin = do_read_plugin(NameVsn), + ensure_apps_stopped(Plugin) + end). + +%% @doc Stop and then start the plugin. +restart(NameVsn) -> + case ensure_stopped(NameVsn) of + ok -> ensure_started(NameVsn); + {error, Reason} -> {error, Reason} + end. + +%% @doc List all installed plugins. +%% Including the ones that are installed, but not enabled in config. +-spec list() -> [plugin()]. +list() -> + Pattern = filename:join([install_dir(), "*", "release.json"]), + All = lists:filtermap( + fun(JsonFile) -> + case read_plugin({file, JsonFile}) of + {ok, Info} -> + {true, Info}; + {error, Reason} -> + ?SLOG(warning, Reason), + false + end + end, filelib:wildcard(Pattern)), + list(configured(), All). + +%% Make sure configured ones are ordered in front. +list([], All) -> All; +list([#{name_vsn := NameVsn} | Rest], All) -> + SplitF = fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) -> + bin([Name, "-", Vsn]) =/= bin(NameVsn) + end, + case lists:splitwith(SplitF, All) of + {_, []} -> + ?SLOG(warning, #{msg => "configured_plugin_not_installed", + name_vsn => NameVsn + }), + list(Rest, All); + {Front, [I | Rear]} -> + [I | list(Rest, Front ++ Rear)] + end. + +do_ensure_started(NameVsn) -> + tryit("start_plugins", + fun() -> + Plugin = do_read_plugin(NameVsn), + ok = load_code_start_apps(NameVsn, Plugin) + end). + +%% try the function, catch 'throw' exceptions as normal 'error' return +%% other exceptions with stacktrace returned. +tryit(WhichOp, F) -> + try + F() + catch + throw : Reason -> + %% thrown exceptions are known errors + %% translate to a return value without stacktrace + {error, Reason}; + error : Reason : Stacktrace -> + %% unexpected errors, log stacktrace + ?SLOG(warning, #{ msg => "plugin_op_failed" + , which_op => WhichOp + , exception => Reason + , stacktrace => Stacktrace + }), + {error, {failed, WhichOp}} + end. + +%% read plugin info from the JSON file +%% returns {ok, Info} or {error, Reason} +read_plugin(NameVsn) -> + tryit("read_plugin_info", + fun() -> {ok, do_read_plugin(NameVsn)} end). + +do_read_plugin({file, InfoFile}) -> + [_, NameVsn | _] = lists:reverse(filename:split(InfoFile)), + case hocon:load(InfoFile, #{format => richmap}) of + {ok, RichMap} -> + Info = check_plugin(hocon_util:richmap_to_map(RichMap), NameVsn, InfoFile), + maps:merge(Info, plugin_status(NameVsn)); + {error, Reason} -> + throw(#{error => "bad_info_file", + path => InfoFile, + return => Reason + }) + end; +do_read_plugin(NameVsn) -> + do_read_plugin({file, info_file(NameVsn)}). + +plugin_status(NameVsn) -> + {AppName, _AppVsn} = parse_name_vsn(NameVsn), + RunningSt = + case application:get_key(AppName, vsn) of + {ok, _} -> + case lists:keyfind(AppName, 1, running_apps()) of + {AppName, _} -> running; + _ -> loaded + end; + undefined -> + stopped + end, + Configured = lists:filtermap( + fun(#{name_vsn := Nv, enable := St}) -> + case bin(Nv) =:= bin(NameVsn) of + true -> {true, St}; + false -> false + end + end, configured()), + ConfSt = case Configured of + [] -> not_configured; + [true] -> enabled; + [false] -> disabled + end, + #{ running_status => RunningSt + , config_status => ConfSt + }. + +bin(A) when is_atom(A) -> atom_to_binary(A, utf8); +bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8); +bin(B) when is_binary(B) -> B. + +check_plugin(#{ <<"name">> := Name + , <<"rel_vsn">> := Vsn + , <<"rel_apps">> := Apps + , <<"description">> := _ + } = Info, NameVsn, File) -> + case bin(NameVsn) =:= bin([Name, "-", Vsn]) of + true -> + try + [_ | _ ] = Apps, %% assert + %% validate if the list is all - strings + lists:foreach(fun parse_name_vsn/1, Apps) + catch + _ : _ -> + throw(#{ error => "bad_rel_apps" + , rel_apps => Apps + , hint => "A non-empty string list of app_name-app_vsn format" + }) + end, + Info; + false -> + throw(#{ error => "name_vsn_mismatch" + , name_vsn => NameVsn + , path => File + , name => Name + , rel_vsn => Vsn + }) + end; +check_plugin(_What, NameVsn, File) -> + throw(#{ error => "bad_info_file_content" + , mandatory_fields => [rel_vsn, name, rel_apps, description] + , name_vsn => NameVsn + , path => File + }). + +load_code_start_apps(RelNameVsn, #{<<"rel_apps">> := Apps}) -> + LibDir = filename:join([install_dir(), RelNameVsn]), + RunningApps = running_apps(), + %% load plugin apps and beam code + AppNames = + lists:map(fun(AppNameVsn) -> + {AppName, AppVsn} = parse_name_vsn(AppNameVsn), + EbinDir = filename:join([LibDir, AppNameVsn, "ebin"]), + ok = load_plugin_app(AppName, AppVsn, EbinDir, RunningApps), + AppName + end, Apps), + lists:foreach(fun start_app/1, AppNames). + +load_plugin_app(AppName, AppVsn, Ebin, RunningApps) -> + case lists:keyfind(AppName, 1, RunningApps) of + false -> do_load_plugin_app(AppName, Ebin); + {_, Vsn} -> + case bin(Vsn) =:= bin(AppVsn) of + true -> + %% already started on the exact versio + ok; + false -> + %% running but a different version + ?SLOG(warning, #{msg => "plugin_app_already_running", name => AppName, + running_vsn => Vsn, + loading_vsn => AppVsn + }) end end. -%% @doc List all available plugins --spec(list() -> [emqx_types:plugin()]). -list() -> - StartedApps = names(started_app), - lists:map(fun({Name, _, _}) -> - Plugin = plugin(Name), - case lists:member(Name, StartedApps) of - true -> Plugin#plugin{active = true}; - false -> Plugin - end - end, lists:sort(ekka_boot:all_module_attributes(emqx_plugin))). - -find_plugin(Name) -> - find_plugin(Name, list()). - -find_plugin(Name, Plugins) -> - lists:keyfind(Name, 2, Plugins). - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -%% load external plugins which are placed in etc/plugins dir -load_ext_plugins(undefined) -> ok; -load_ext_plugins(Dir) -> - lists:foreach( - fun(Plugin) -> - PluginDir = filename:join(Dir, Plugin), - case filelib:is_dir(PluginDir) of - true -> load_ext_plugin(PluginDir); - false -> ok - end - end, filelib:wildcard("*", Dir)). - -load_ext_plugin(PluginDir) -> - ?SLOG(debug, #{msg => "loading_extra_plugin", plugin_dir => PluginDir}), - Ebin = filename:join([PluginDir, "ebin"]), - AppFile = filename:join([Ebin, "*.app"]), - AppName = case filelib:wildcard(AppFile) of - [App] -> - list_to_atom(filename:basename(App, ".app")); - [] -> - ?SLOG(alert, #{msg => "plugin_app_file_not_found", app_file => AppFile}), - error({plugin_app_file_not_found, AppFile}) - end, - ok = load_plugin_app(AppName, Ebin). - % try - % ok = generate_configs(AppName, PluginDir) - % catch - % throw : {conf_file_not_found, ConfFile} -> - % %% this is maybe a dependency of an external plugin - % ?LOG(debug, "config_load_error_ignored for app=~p, path=~ts", [AppName, ConfFile]), - % ok - % end. - -load_plugin_app(AppName, Ebin) -> +do_load_plugin_app(AppName, Ebin) when is_binary(Ebin) -> + do_load_plugin_app(AppName, binary_to_list(Ebin)); +do_load_plugin_app(AppName, Ebin) -> _ = code:add_patha(Ebin), Modules = filelib:wildcard(filename:join([Ebin, "*.beam"])), lists:foreach( @@ -162,103 +439,160 @@ load_plugin_app(AppName, Ebin) -> Module = list_to_atom(filename:basename(BeamFile, ".beam")), case code:load_file(Module) of {module, _} -> ok; - {error, Reason} -> error({failed_to_load_plugin_beam, BeamFile, Reason}) + {error, Reason} -> throw(#{error => "failed_to_load_plugin_beam", + path => BeamFile, + reason => Reason + }) end end, Modules), case application:load(AppName) of ok -> ok; - {error, {already_loaded, _}} -> ok - end. - -%% Stop plugins -stop_plugins(Plugins) -> - _ = [stop_app(Plugin#plugin.name) || Plugin <- Plugins], - ok. - -plugin(AppName) -> - case application:get_all_key(AppName) of - {ok, Attrs} -> - Descr = proplists:get_value(description, Attrs, ""), - #plugin{name = AppName, descr = Descr}; - undefined -> error({plugin_not_found, AppName}) - end. - -load_plugin(Name) -> - try - case load_app(Name) of - ok -> - start_app(Name); - {error, Error0} -> - {error, Error0} - end - catch Error : Reason : Stacktrace -> - ?SLOG(alert, #{ - msg => "plugin_load_failed", - name => Name, - exception => Error, - reason => Reason, - stacktrace => Stacktrace - }), - {error, parse_config_file_failed} - end. - -load_app(App) -> - case application:load(App) of - ok -> - ok; - {error, {already_loaded, App}} -> - ok; - {error, Error} -> - {error, Error} + {error, {already_loaded, _}} -> ok; + {error, Reason} -> throw(#{error => "failed_to_load_plugin_app", + name => AppName, + reason => Reason}) end. start_app(App) -> case application:ensure_all_started(App) of {ok, Started} -> case Started =/= [] of - true -> ?SLOG(info, #{msg => "started_plugin_dependency_apps", apps => Started}); + true -> ?SLOG(debug, #{msg => "started_plugin_apps", apps => Started}); false -> ok end, - ?SLOG(info, #{msg => "started_plugin_app", app => App}), + ?SLOG(debug, #{msg => "started_plugin_app", app => App}), ok; {error, {ErrApp, Reason}} -> - ?SLOG(error, #{msg => failed_to_start_plugin_app, - app => App, - err_app => ErrApp, - reason => Reason - }), - {error, failed_to_start_plugin_app} + throw(#{error => "failed_to_start_plugin_app", + app => App, + err_app => ErrApp, + reason => Reason + }) end. -unload_plugin(App) -> - case stop_app(App) of - ok -> +%% Stop all apps installed by the plugin package, +%% but not the ones shared with others. +ensure_apps_stopped(#{<<"rel_apps">> := Apps}) -> + %% load plugin apps and beam code + AppsToStop = + lists:map(fun(NameVsn) -> + {AppName, _AppVsn} = parse_name_vsn(NameVsn), + AppName + end, Apps), + case tryit("stop_apps", fun() -> stop_apps(AppsToStop) end) of + {ok, []} -> + %% all apps stopped + ok; + {ok, Left} -> + ?SLOG(warning, #{msg => "unabled_to_stop_plugin_apps", + apps => Left + }), ok; {error, Reason} -> {error, Reason} end. +stop_apps(Apps) -> + RunningApps = running_apps(), + case do_stop_apps(Apps, [], RunningApps) of + {ok, []} -> {ok, []}; %% all stopped + {ok, Remain} when Remain =:= Apps -> {ok, Apps}; %% no progress + {ok, Remain} -> stop_apps(Remain) %% try again + end. + +do_stop_apps([], Remain, _AllApps) -> + {ok, lists:reverse(Remain)}; +do_stop_apps([App | Apps], Remain, RunningApps) -> + case is_needed_by_any(App, RunningApps) of + true -> + do_stop_apps(Apps, [App | Remain], RunningApps); + false -> + ok = stop_app(App), + do_stop_apps(Apps, Remain, RunningApps) + end. + stop_app(App) -> case application:stop(App) of ok -> - ?SLOG(info, #{msg => "stop_plugin_successfully", app => App}), - ok; + ?SLOG(debug, #{msg => "stop_plugin_successfully", app => App}), + ok = unload_moudle_and_app(App); {error, {not_started, App}} -> - ?SLOG(info, #{msg => "plugin_not_started", app => App}), - ok; + ?SLOG(debug, #{msg => "plugin_not_started", app => App}), + ok = unload_moudle_and_app(App); {error, Reason} -> - ?SLOG(error, #{msg => "failed_to_stop_plugin_app", - app => App, - error => Reason - }), - {error, Reason} + throw(#{error => "failed_to_stop_app", app => App, reason => Reason}) end. -names(plugin) -> - names(list()); +unload_moudle_and_app(App) -> + case application:get_key(App, modules) of + {ok, Modules} -> lists:foreach(fun code:soft_purge/1, Modules); + _ -> ok + end, + _ = application:unload(App), + ok. -names(started_app) -> - [Name || {Name, _Descr, _Ver} <- application:which_applications()]; +is_needed_by_any(AppToStop, RunningApps) -> + lists:any(fun({RunningApp, _RunningAppVsn}) -> + is_needed_by(AppToStop, RunningApp) + end, RunningApps). -names(Plugins) -> - [Name || #plugin{name = Name} <- Plugins]. +is_needed_by(AppToStop, AppToStop) -> false; +is_needed_by(AppToStop, RunningApp) -> + case application:get_key(RunningApp, applications) of + {ok, Deps} -> lists:member(AppToStop, Deps); + undefined -> false + end. + +put_config(Key, Value) when is_atom(Key) -> + put_config([Key], Value); +put_config(Path, Value) when is_list(Path) -> + emqx_config:put([?CONF_ROOT | Path], Value). + +get_config(Key, Default) when is_atom(Key) -> + get_config([Key], Default); +get_config(Path, Default) -> + emqx:get_config([?CONF_ROOT | Path], Default). + +install_dir() -> get_config(install_dir, ""). + +put_configured(Configured) -> + ok = put_config(states, Configured). + +configured() -> + get_config(states, []). + +for_plugins(ActionFun) -> + case lists:flatmap(fun(I) -> for_plugin(I, ActionFun) end, configured()) of + [] -> ok; + Errors -> erlang:error(#{function => ActionFun, errors => Errors}) + end. + +for_plugin(#{name_vsn := NameVsn, enable := true}, Fun) -> + case Fun(NameVsn) of + ok -> []; + {error, Reason} -> [{NameVsn, Reason}] + end; +for_plugin(#{name_vsn := NameVsn, enable := false}, _Fun) -> + ?SLOG(debug, #{msg => "plugin_disabled", + name_vsn => NameVsn}), + []. + +parse_name_vsn(NameVsn) when is_binary(NameVsn) -> + parse_name_vsn(binary_to_list(NameVsn)); +parse_name_vsn(NameVsn) when is_list(NameVsn) -> + {AppName, [$- | Vsn]} = lists:splitwith(fun(X) -> X =/= $- end, NameVsn), + {list_to_atom(AppName), Vsn}. + +pkg_file(NameVsn) -> + filename:join([install_dir(), bin([NameVsn, ".tar.gz"])]). + +dir(NameVsn) -> + filename:join([install_dir(), NameVsn]). + +info_file(NameVsn) -> + filename:join([dir(NameVsn), "release.json"]). + +running_apps() -> + lists:map(fun({N, _, V}) -> + {N, V} + end, application:which_applications(infinity)). diff --git a/apps/emqx_plugins/src/emqx_plugins_app.erl b/apps/emqx_plugins/src/emqx_plugins_app.erl index c04fbb445..ef62a6b42 100644 --- a/apps/emqx_plugins/src/emqx_plugins_app.erl +++ b/apps/emqx_plugins/src/emqx_plugins_app.erl @@ -24,6 +24,7 @@ start(_Type, _Args) -> {ok, Sup} = emqx_plugins_sup:start_link(), + ok = emqx_plugins:ensure_started(), %% load all pre-configured {ok, Sup}. stop(_State) -> diff --git a/apps/emqx_plugins/src/emqx_plugins_cli.erl b/apps/emqx_plugins/src/emqx_plugins_cli.erl new file mode 100644 index 000000000..694089624 --- /dev/null +++ b/apps/emqx_plugins/src/emqx_plugins_cli.erl @@ -0,0 +1,88 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 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_plugins_cli). + +-export([ list/1 + , describe/2 + , ensure_installed/2 + , ensure_uninstalled/2 + , ensure_started/2 + , ensure_stopped/2 + , restart/2 + , ensure_disabled/2 + , ensure_enabled/3 + ]). + +-include_lib("emqx/include/logger.hrl"). + +-define(PRINT(EXPR, LOG_FUN), + print(NameVsn, fun()-> EXPR end(), LOG_FUN, ?FUNCTION_NAME)). + +list(LogFun) -> + LogFun("~ts~n", [to_json(emqx_plugins:list())]). + +describe(NameVsn, LogFun) -> + case emqx_plugins:describe(NameVsn) of + {ok, Plugin} -> + LogFun("~ts~n", [to_json(Plugin)]); + {error, Reason} -> + %% this should not happend unless the package is manually installed + %% corrupted packages installed from emqx_plugins:ensure_installed + %% should not leave behind corrupted files + ?SLOG(error, #{msg => "failed_to_describe_plugin", + name_vsn => NameVsn, + cause => Reason}), + %% do nothing to the CLI console + ok + end. + +ensure_installed(NameVsn, LogFun) -> + ?PRINT(emqx_plugins:ensure_installed(NameVsn), LogFun). + +ensure_uninstalled(NameVsn, LogFun) -> + ?PRINT(emqx_plugins:ensure_uninstalled(NameVsn), LogFun). + +ensure_started(NameVsn, LogFun) -> + ?PRINT(emqx_plugins:ensure_started(NameVsn), LogFun). + +ensure_stopped(NameVsn, LogFun) -> + ?PRINT(emqx_plugins:ensure_stopped(NameVsn), LogFun). + +restart(NameVsn, LogFun) -> + ?PRINT(emqx_plugins:restart(NameVsn), LogFun). + +ensure_enabled(NameVsn, Position, LogFun) -> + ?PRINT(emqx_plugins:ensure_enabled(NameVsn, Position), LogFun). + +ensure_disabled(NameVsn, LogFun) -> + ?PRINT(emqx_plugins:ensure_disabled(NameVsn), LogFun). + +to_json(Input) -> + emqx_logger_jsonfmt:best_effort_json(Input). + +print(NameVsn, Res, LogFun, Action) -> + Obj = #{action => Action, + name_vsn => NameVsn}, + JsonReady = + case Res of + ok -> + Obj#{result => ok}; + {error, Reason} -> + Obj#{result => not_ok, + cause => Reason} + end, + LogFun("~ts~n", [to_json(JsonReady)]). diff --git a/apps/emqx_plugins/src/emqx_plugins_schema.erl b/apps/emqx_plugins/src/emqx_plugins_schema.erl index 8d04923ff..6666e7081 100644 --- a/apps/emqx_plugins/src/emqx_plugins_schema.erl +++ b/apps/emqx_plugins/src/emqx_plugins_schema.erl @@ -20,14 +20,18 @@ -export([ roots/0 , fields/1 + , namespace/0 ]). -include_lib("typerefl/include/types.hrl"). +-include("emqx_plugins.hrl"). -roots() -> ["plugins"]. +namespace() -> "plugin". -fields("plugins") -> - #{fields => fields(), +roots() -> [?CONF_ROOT]. + +fields(?CONF_ROOT) -> + #{fields => root_fields(), desc => """ Manage EMQ X plugins.
@@ -37,44 +41,39 @@ or installed as a standalone package in a location specified by
The standalone-installed plugins are referred to as 'external' plugins. """ + }; +fields(state) -> + #{ fields => state_fields(), + desc => "A per-plugin config to describe the desired state of the plugin." }. -fields() -> - [ {prebuilt, fun prebuilt/1} - , {external, fun external/1} +state_fields() -> + [ {name_vsn, + hoconsc:mk(string(), + #{ desc => "The {name}-{version} of the plugin.
" + "It should match the plugin application name-vsn as the " + "for the plugin release package name
" + "For example: my_plugin-0.1.0." + , nullable => false + })} + , {enable, + hoconsc:mk(boolean(), + #{ desc => "Set to 'true' to enable this plugin" + , nullable => false + })} + ]. + +root_fields() -> + [ {states, fun states/1} , {install_dir, fun install_dir/1} ]. -prebuilt(type) -> hoconsc:map("name", boolean()); -prebuilt(nullable) -> true; -prebuilt(T) when T=/= desc -> undefined; -prebuilt(desc) -> """ -A map() from plugin name to a boolean (true | false) flag to indicate -whether or not to enable the prebuilt plugin. -
-Most of the prebuilt plugins from 4.x are converted into features since 5.0. -""" ++ prebuilt_plugins() ++ -""" -
-Enabled plugins are loaded (started) as a part of EMQ X node's boot sequence. -Plugins can be loaded on the fly, and enabled from dashbaord UI and/or CLI. -
-Example config: {emqx_foo_bar: true, emqx_bazz: false} -""". - -external(type) -> hoconsc:map("name", string()); -external(nullable) -> true; -external(T) when T =/= desc -> undefined; -external(desc) -> -""" -A map from plugin name to a version number string for enabled ones. -To disable an external plugin, set the value to 'false'. -
-Enabled plugins are loaded (started) as a part of EMQ X node's boot sequence. -Plugins can be loaded on the fly, and enabled from dashbaord UI and/or CLI. -
-Example config: {emqx_extplug1: \"0.1.0\", emqx_extplug2: false} -""". +states(type) -> hoconsc:array(hoconsc:ref(state)); +states(nullable) -> true; +states(default) -> []; +states(desc) -> "An array of plugins in the desired states.
" + "The plugins are started in the defined order"; +states(_) -> undefined. install_dir(type) -> string(); install_dir(nullable) -> true; @@ -88,12 +87,3 @@ the sub-directory named as emqx_foo_bar-0.1.0. NOTE: For security reasons, this directory should **NOT** be writable by anyone expect for emqx (or any user which runs EMQ X) """. - -%% TODO: when we have some prebuilt plugins, change this function to: -%% """ -%% The names should be one of -%% - name1 -%% - name2 -%% """ -prebuilt_plugins() -> - "So far, we do not have any prebuilt plugins". diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE.erl b/apps/emqx_plugins/test/emqx_plugins_SUITE.erl index 013544e32..b53fe461d 100644 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE.erl +++ b/apps/emqx_plugins/test/emqx_plugins_SUITE.erl @@ -22,92 +22,254 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("eunit/include/eunit.hrl"). +-define(EMQX_PLUGIN_TEMPLATE_VSN, "5.0-rc.1"). +-define(PACKAGE_SUFFIX, ".tar.gz"). + all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> - - %% Compile extra plugin code - - DataPath = proplists:get_value(data_dir, Config), - AppPath = filename:join([DataPath, "emqx_mini_plugin"]), - HoconPath = filename:join([DataPath, "emqx_hocon_plugin"]), - Cmd = lists:flatten(io_lib:format("cd ~ts && make", [AppPath])), - CmdPath = lists:flatten(io_lib:format("cd ~ts && make", [HoconPath])), - - ct:pal("Executing ~ts~n", [Cmd]), - ct:pal("~n ~ts~n", [os:cmd(Cmd)]), - - ct:pal("Executing ~ts~n", [CmdPath]), - ct:pal("~n ~ts~n", [os:cmd(CmdPath)]), - - emqx_common_test_helpers:boot_modules([]), + WorkDir = proplists:get_value(data_dir, Config), + OrigInstallDir = emqx_plugins:get_config(install_dir, undefined), + emqx_plugins:put_config(install_dir, WorkDir), emqx_common_test_helpers:start_apps([]), - emqx_config:put([plugins, install_dir], DataPath), - ?assertEqual(ok, emqx_plugins:load()), - Config. + [{orig_install_dir, OrigInstallDir} | Config]. -end_per_suite(_Config) -> +end_per_suite(Config) -> emqx_common_test_helpers:boot_modules(all), emqx_common_test_helpers:stop_apps([]), - emqx_config:erase(plugins). + emqx_config:erase(plugins), + %% restore config + case proplists:get_value(orig_install_dir, Config) of + undefined -> ok; + OrigInstallDir -> emqx_plugins:put_config(install_dir, OrigInstallDir) + end. -t_load(_) -> - ?assertEqual(ok, emqx_plugins:load()), - ?assertEqual(ok, emqx_plugins:unload()), +init_per_testcase(TestCase, Config) -> + emqx_plugins:put_configured([]), + lists:foreach(fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) -> + emqx_plugins:purge(bin([Name, "-", Vsn])) + end, emqx_plugins:list()), + ?MODULE:TestCase({init, Config}). - ?assertEqual({error, not_found}, emqx_plugins:load(not_existed_plugin)), - ?assertEqual({error, not_started}, emqx_plugins:unload(emqx_mini_plugin)), - ?assertEqual({error, not_started}, emqx_plugins:unload(emqx_hocon_plugin)), +end_per_testcase(TestCase, Config) -> + emqx_plugins:put_configured([]), + ?MODULE:TestCase({'end', Config}). - emqx_config:erase(plugins). +build_demo_plugin_package() -> + WorkDir = emqx_plugins:install_dir(), + BuildSh = filename:join([WorkDir, "build-demo-plugin.sh"]), + case emqx_run_sh:do(BuildSh ++ " " ++ ?EMQX_PLUGIN_TEMPLATE_VSN, + [{cd, WorkDir}]) of + {ok, _} -> + Pkg = filename:join([WorkDir, "emqx_plugin_template-" ++ + ?EMQX_PLUGIN_TEMPLATE_VSN ++ + ?PACKAGE_SUFFIX]), + case filelib:is_regular(Pkg) of + true -> Pkg; + false -> error(#{reason => unexpected_build_result, not_found => Pkg}) + end; + {error, {Rc, Output}} -> + io:format(user, "failed_to_build_demo_plugin, Exit = ~p, Output:~n~ts\n", [Rc, Output]), + error(failed_to_build_demo_plugin) + end. -t_load_ext_plugin(_) -> - ?assertError({plugin_app_file_not_found, _}, - emqx_plugins:load_ext_plugin("./not_existed_path/")). +bin(A) when is_atom(A) -> atom_to_binary(A, utf8); +bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8); +bin(B) when is_binary(B) -> B. -t_list(_) -> - ?assertMatch([{plugin, _, _, _, _, _, _} | _ ], emqx_plugins:list()). +t_demo_install_start_stop_uninstall({init, Config}) -> + Package = build_demo_plugin_package(), + NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX), + [{name_vsn, NameVsn} | Config]; +t_demo_install_start_stop_uninstall({'end', _Config}) -> ok; +t_demo_install_start_stop_uninstall(Config) -> + NameVsn = proplists:get_value(name_vsn, Config), + ok = emqx_plugins:ensure_installed(NameVsn), + %% idempotent + ok = emqx_plugins:ensure_installed(NameVsn), + {ok, Info} = emqx_plugins:read_plugin(NameVsn), + ?assertEqual([Info], emqx_plugins:list()), + %% start + ok = emqx_plugins:ensure_started(NameVsn), + ok = assert_app_running(emqx_plugin_template, true), + ok = assert_app_running(map_sets, true), + %% start (idempotent) + ok = emqx_plugins:ensure_started(bin(NameVsn)), + ok = assert_app_running(emqx_plugin_template, true), + ok = assert_app_running(map_sets, true), -t_find_plugin(_) -> - ?assertMatch({plugin, emqx_mini_plugin, _, _, _, _, _}, emqx_plugins:find_plugin(emqx_mini_plugin)), - ?assertMatch({plugin, emqx_hocon_plugin, _, _, _, _, _}, emqx_plugins:find_plugin(emqx_hocon_plugin)). + %% running app can not be un-installed + ?assertMatch({error, _}, + emqx_plugins:ensure_uninstalled(NameVsn)), -t_plugin(_) -> + %% stop + ok = emqx_plugins:ensure_stopped(NameVsn), + ok = assert_app_running(emqx_plugin_template, false), + ok = assert_app_running(map_sets, false), + %% stop (idempotent) + ok = emqx_plugins:ensure_stopped(bin(NameVsn)), + ok = assert_app_running(emqx_plugin_template, false), + ok = assert_app_running(map_sets, false), + %% still listed after stopped + ?assertMatch([#{<<"name">> := <<"emqx_plugin_template">>, + <<"rel_vsn">> := <> + }], emqx_plugins:list()), + ok = emqx_plugins:ensure_uninstalled(NameVsn), + ?assertEqual([], emqx_plugins:list()), + ok. + +%% help funtion to create a info file. +%% The file is in JSON format when built +%% but since we are using hocon:load to load it +%% ad-hoc test files can be in hocon format +write_info_file(Config, NameVsn, Content) -> + WorkDir = proplists:get_value(data_dir, Config), + InfoFile = filename:join([WorkDir, NameVsn, "release.json"]), + ok = filelib:ensure_dir(InfoFile), + ok = file:write_file(InfoFile, Content). + +t_start_restart_and_stop({init, Config}) -> + Package = build_demo_plugin_package(), + NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX), + [{name_vsn, NameVsn} | Config]; +t_start_restart_and_stop({'end', _Config}) -> ok; +t_start_restart_and_stop(Config) -> + NameVsn = proplists:get_value(name_vsn, Config), + ok = emqx_plugins:ensure_installed(NameVsn), + ok = emqx_plugins:ensure_enabled(NameVsn), + FakeInfo = "name=bar, rel_vsn=\"2\", rel_apps=[\"bar-9\"]," + "description=\"desc bar\"", + Bar2 = <<"bar-2">>, + ok = write_info_file(Config, Bar2, FakeInfo), + %% fake a disabled plugin in config + ok = emqx_plugins:ensure_state(Bar2, front, false), + + assert_app_running(emqx_plugin_template, false), + ok = emqx_plugins:ensure_started(), + assert_app_running(emqx_plugin_template, true), + + %% fake enable bar-2 + ok = emqx_plugins:ensure_state(Bar2, rear, true), + %% should cause an error + ?assertError(#{function := _, errors := [_ | _]}, + emqx_plugins:ensure_started()), + %% but demo plugin should still be running + assert_app_running(emqx_plugin_template, true), + + %% stop all + ok = emqx_plugins:ensure_stopped(), + assert_app_running(emqx_plugin_template, false), + ok = emqx_plugins:ensure_state(Bar2, rear, false), + + ok = emqx_plugins:restart(NameVsn), + assert_app_running(emqx_plugin_template, true), + %% repeat + ok = emqx_plugins:restart(NameVsn), + assert_app_running(emqx_plugin_template, true), + + ok = emqx_plugins:ensure_stopped(), + ok = emqx_plugins:ensure_disabled(NameVsn), + ok = emqx_plugins:ensure_uninstalled(NameVsn), + ok = emqx_plugins:ensure_uninstalled(Bar2), + ?assertEqual([], emqx_plugins:list()), + ok. + +t_enable_disable({init, Config}) -> + Package = build_demo_plugin_package(), + NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX), + [{name_vsn, NameVsn} | Config]; +t_enable_disable({'end', Config}) -> + ok = emqx_plugins:ensure_uninstalled(proplists:get_value(name_vsn, Config)); +t_enable_disable(Config) -> + NameVsn = proplists:get_value(name_vsn, Config), + ok = emqx_plugins:ensure_installed(NameVsn), + ?assertEqual([], emqx_plugins:configured()), + ok = emqx_plugins:ensure_enabled(NameVsn), + ?assertEqual([#{name_vsn => NameVsn, enable => true}], emqx_plugins:configured()), + ok = emqx_plugins:ensure_disabled(NameVsn), + ?assertEqual([#{name_vsn => NameVsn, enable => false}], emqx_plugins:configured()), + ok = emqx_plugins:ensure_enabled(bin(NameVsn)), + ?assertEqual([#{name_vsn => NameVsn, enable => true}], emqx_plugins:configured()), + ?assertMatch({error, #{reason := "bad_plugin_config_status", + hint := "disable_the_plugin_first" + }}, emqx_plugins:ensure_uninstalled(NameVsn)), + ok = emqx_plugins:ensure_disabled(bin(NameVsn)), + ok = emqx_plugins:ensure_uninstalled(NameVsn), + ?assertMatch({error, _}, emqx_plugins:ensure_enabled(NameVsn)), + ?assertMatch({error, _}, emqx_plugins:ensure_disabled(NameVsn)), + ok. + +assert_app_running(Name, true) -> + AllApps = application:which_applications(), + ?assertMatch({Name, _, _}, lists:keyfind(Name, 1, AllApps)); +assert_app_running(Name, false) -> + AllApps = application:which_applications(), + ?assertEqual(false, lists:keyfind(Name, 1, AllApps)). + +t_bad_tar_gz({init, Config}) -> Config; +t_bad_tar_gz({'end', _Config}) -> ok; +t_bad_tar_gz(Config) -> + WorkDir = proplists:get_value(data_dir, Config), + FakeTarTz = filename:join([WorkDir, "fake-vsn.tar.gz"]), + ok = file:write_file(FakeTarTz, "a\n"), + ?assertMatch({error, #{reason := "bad_plugin_package", + return := eof + }}, + emqx_plugins:ensure_installed("fake-vsn")), + ?assertMatch({error, #{reason := "failed_to_extract_plugin_package", + return := not_found + }}, + emqx_plugins:ensure_installed("nonexisting")), + ?assertEqual([], emqx_plugins:list()), + ok = emqx_plugins:delete_package("fake-vsn"), + %% idempotent + ok = emqx_plugins:delete_package("fake-vsn"). + +%% create a corrupted .tar.gz +%% failed install attempts should not leave behind extracted dir +t_bad_tar_gz2({init, Config}) -> Config; +t_bad_tar_gz2({'end', _Config}) -> ok; +t_bad_tar_gz2(Config) -> + WorkDir = proplists:get_value(data_dir, Config), + NameVsn = "foo-0.2", + %% this an invalid info file content + BadInfo = "name=foo, rel_vsn=\"0.2\", rel_apps=[foo]", + ok = write_info_file(Config, NameVsn, BadInfo), + TarGz = filename:join([WorkDir, NameVsn ++ ".tar.gz"]), + ok = make_tar(WorkDir, NameVsn), + ?assert(filelib:is_regular(TarGz)), + %% failed to install, it also cleans up the bad .tar.gz file + ?assertMatch({error, _}, emqx_plugins:ensure_installed(NameVsn)), + %% the tar.gz file is still around + ?assert(filelib:is_regular(TarGz)), + ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))), + ok = emqx_plugins:delete_package(NameVsn). + +t_bad_info_json({init, Config}) -> Config; +t_bad_info_json({'end', _}) -> ok; +t_bad_info_json(Config) -> + NameVsn = "test-2", + ok = write_info_file(Config, NameVsn, "bad-syntax"), + ?assertMatch({error, #{error := "bad_info_file", + return := {parse_error, _} + }}, + emqx_plugins:read_plugin(NameVsn)), + ok = write_info_file(Config, NameVsn, "{\"bad\": \"obj\"}"), + ?assertMatch({error, #{error := "bad_info_file_content", + mandatory_fields := _ + }}, + emqx_plugins:read_plugin(NameVsn)), + ?assertEqual([], emqx_plugins:list()), + emqx_plugins:purge(NameVsn), + ok. + +make_tar(Cwd, NameWithVsn) -> + {ok, OriginalCwd} = file:get_cwd(), + ok = file:set_cwd(Cwd), try - emqx_plugins:plugin(not_existed_plugin) - catch - _Error:Reason:_Stacktrace -> - ?assertEqual({plugin_not_found,not_existed_plugin}, Reason) - end, - ?assertMatch({plugin, emqx_mini_plugin, _, _, _, _, _}, emqx_plugins:plugin(emqx_mini_plugin)), - ?assertMatch({plugin, emqx_hocon_plugin, _, _, _, _, _}, emqx_plugins:plugin(emqx_hocon_plugin)). - -t_load_plugin(_) -> - ok = meck:new(application, [unstick, non_strict, passthrough, no_history]), - ok = meck:expect(application, load, fun(already_loaded_app) -> {error, {already_loaded, already_loaded_app}}; - (error_app) -> {error, error}; - (_) -> ok end), - ok = meck:expect(application, ensure_all_started, fun(already_loaded_app) -> {error, {already_loaded_app, already_loaded}}; - (error_app) -> {error, error}; - (App) -> {ok, App} end), - ok = meck:new(emqx_plugins, [unstick, non_strict, passthrough, no_history]), - ok = meck:expect(emqx_plugins, generate_configs, fun(_) -> ok end), - ok = meck:expect(emqx_plugins, apply_configs, fun(_) -> ok end), - ?assertMatch({error, _}, emqx_plugins:load_plugin(already_loaded_app)), - ?assertMatch(ok, emqx_plugins:load_plugin(normal)), - ?assertMatch({error,_}, emqx_plugins:load_plugin(error_app)), - - ok = meck:unload(emqx_plugins), - ok = meck:unload(application). - -t_unload_plugin(_) -> - ok = meck:new(application, [unstick, non_strict, passthrough, no_history]), - ok = meck:expect(application, stop, fun(not_started_app) -> {error, {not_started, not_started_app}}; - (error_app) -> {error, error}; - (_) -> ok end), - - ?assertEqual(ok, emqx_plugins:unload_plugin(not_started_app)), - ?assertEqual(ok, emqx_plugins:unload_plugin(normal)), - ?assertEqual({error,error}, emqx_plugins:unload_plugin(error_app)), - - ok = meck:unload(application). + Files = filelib:wildcard(NameWithVsn ++ "/**"), + TarFile = NameWithVsn ++ ".tar.gz", + ok = erl_tar:create(TarFile, Files, [compressed]) + after + file:set_cwd(OriginalCwd) + end. diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/build-demo-plugin.sh b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/build-demo-plugin.sh new file mode 100755 index 000000000..fe757396b --- /dev/null +++ b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/build-demo-plugin.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -euo pipefail + +vsn="${1}" +workdir="demo_src" +target_name="emqx_plugin_template-${vsn}.tar.gz" +target="$workdir/_build/default/emqx_plugrel/${target_name}" +if [ -f "${target}" ]; then + cp "$target" ./ + exit 0 +fi + +# cleanup +rm -rf "${workdir}" + +git clone https://github.com/emqx/emqx-plugin-template.git -b "${vsn}" ${workdir} +make -C "$workdir" rel + +cp "$target" ./ diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/Makefile b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/Makefile deleted file mode 100644 index e0ed250e1..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/Makefile +++ /dev/null @@ -1,26 +0,0 @@ -## shallow clone for speed - -REBAR_GIT_CLONE_OPTIONS += --depth 1 -export REBAR_GIT_CLONE_OPTIONS - -REBAR = rebar3 -all: compile - -compile: - $(REBAR) compile - cp -r _build/default/lib/emqx_hocon_plugin/ebin ./ - -clean: distclean - -ct: compile - $(REBAR) as test ct -v - -eunit: compile - $(REBAR) as test eunit - -xref: - $(REBAR) xref - -distclean: - @rm -rf _build - @rm -f ebin/ data/app.*.config data/vm.*.args rebar.lock diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/etc/emqx_hocon_plugin.conf b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/etc/emqx_hocon_plugin.conf deleted file mode 100644 index 991afdfdd..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/etc/emqx_hocon_plugin.conf +++ /dev/null @@ -1,3 +0,0 @@ -emqx_hocon_plugin { - name = test -} diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/rebar.config b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/rebar.config deleted file mode 100644 index 0e9fe1067..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/rebar.config +++ /dev/null @@ -1,23 +0,0 @@ -{deps, [{hocon, {git, "https://github.com/emqx/hocon", {tag, "0.6.0"}}}]}. - -{edoc_opts, [{preprocess, true}]}. -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - {parse_transform}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. - -{profiles, - [{test, [ - {deps, [ - ]} - ]} -]}. diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin.app.src b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin.app.src deleted file mode 100644 index 03cf8e69c..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin.app.src +++ /dev/null @@ -1,16 +0,0 @@ -%% -*- mode: erlang -*- -{application, emqx_hocon_plugin, - [{description, "An EMQ X plugin for hocon testcase"}, - {vsn, "0.1"}, - {modules, []}, - {registered, []}, - {mod, {emqx_hocon_plugin_app, []}}, - {applications, - [kernel, - stdlib, - typerefl - ]}, - {env,[]}, - {licenses, ["Apache 2.0"]}, - {links, []} - ]}. diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_app.erl b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_app.erl deleted file mode 100644 index 04191aac8..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_app.erl +++ /dev/null @@ -1,42 +0,0 @@ -%%%------------------------------------------------------------------- -%% @doc emqx_mini_plugin public API -%% @end -%%%------------------------------------------------------------------- - --module(emqx_hocon_plugin_app). - --behaviour(application). --behaviour(supervisor). - --emqx_plugin(?MODULE). - -%% Application APIs --export([ start/2 - , stop/1 - ]). - -%% Supervisor callback --export([init/1]). - - -%% -- Application - -start(_StartType, _StartArgs) -> - {ok, Sup} = start_link(), - {ok, Sup}. - -stop(_State) -> - ok. - -%% --- Supervisor - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - SupFlags = #{strategy => one_for_all, - intensity => 0, - period => 1}, - ChildSpecs = [], - {ok, {SupFlags, ChildSpecs}}. - diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_schema.erl b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_schema.erl deleted file mode 100644 index 8e333a3e0..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_schema.erl +++ /dev/null @@ -1,15 +0,0 @@ --module(emqx_hocon_plugin_schema). - --include_lib("typerefl/include/types.hrl"). - --export([roots/0, fields/1]). - --behaviour(hocon_schema). - -roots() -> ["emqx_hocon_plugin"]. - -fields("emqx_hocon_plugin") -> - [{name, fun name/1}]. - -name(type) -> binary(); -name(_) -> undefined. diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/Makefile b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/Makefile deleted file mode 100644 index fd38ff640..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/Makefile +++ /dev/null @@ -1,26 +0,0 @@ -## shallow clone for speed - -REBAR_GIT_CLONE_OPTIONS += --depth 1 -export REBAR_GIT_CLONE_OPTIONS - -REBAR = rebar3 -all: compile - -compile: - $(REBAR) compile - cp -r _build/default/lib/emqx_mini_plugin/ebin ./ - -clean: distclean - -ct: compile - $(REBAR) as test ct -v - -eunit: compile - $(REBAR) as test eunit - -xref: - $(REBAR) xref - -distclean: - @rm -rf _build - @rm -f ebin/ data/app.*.config data/vm.*.args rebar.lock diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/etc/emqx_mini_plugin.conf b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/etc/emqx_mini_plugin.conf deleted file mode 100644 index 423ea099f..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/etc/emqx_mini_plugin.conf +++ /dev/null @@ -1 +0,0 @@ -mini.name = test diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/priv/emqx_mini_plugin.schema b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/priv/emqx_mini_plugin.schema deleted file mode 100644 index f936265c1..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/priv/emqx_mini_plugin.schema +++ /dev/null @@ -1,5 +0,0 @@ -%%-*- mode: erlang -*- - -{mapping, "mini.name", "emqx_mini_plugin.name", [ - {datatype, string} -]}. diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config deleted file mode 100644 index 239726dc0..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config +++ /dev/null @@ -1,23 +0,0 @@ -{deps, []}. - -{edoc_opts, [{preprocess, true}]}. -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - {parse_transform}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. - -{profiles, - [{test, [ - {deps, [ - ]} - ]} -]}. diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin.app.src b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin.app.src deleted file mode 100644 index 0a1806f9c..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin.app.src +++ /dev/null @@ -1,15 +0,0 @@ -%% -*- mode: erlang -*- -{application, emqx_mini_plugin, - [{description, "An EMQ X plugin for testcase"}, - {vsn, "0.1"}, - {modules, []}, - {registered, []}, - {mod, {emqx_mini_plugin_app, []}}, - {applications, - [kernel, - stdlib - ]}, - {env,[]}, - {licenses, ["Apache 2.0"]}, - {links, []} - ]}. diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin_app.erl b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin_app.erl deleted file mode 100644 index 6801a10e3..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin_app.erl +++ /dev/null @@ -1,42 +0,0 @@ -%%%------------------------------------------------------------------- -%% @doc emqx_mini_plugin public API -%% @end -%%%------------------------------------------------------------------- - --module(emqx_mini_plugin_app). - --behaviour(application). --behaviour(supervisor). - --emqx_plugin(?MODULE). - -%% Application APIs --export([ start/2 - , stop/1 - ]). - -%% Supervisor callback --export([init/1]). - - -%% -- Application - -start(_StartType, _StartArgs) -> - {ok, Sup} = start_link(), - {ok, Sup}. - -stop(_State) -> - ok. - -%% --- Supervisor - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - SupFlags = #{strategy => one_for_all, - intensity => 0, - period => 1}, - ChildSpecs = [], - {ok, {SupFlags, ChildSpecs}}. - diff --git a/apps/emqx_plugins/test/emqx_plugins_tests.erl b/apps/emqx_plugins/test/emqx_plugins_tests.erl new file mode 100644 index 000000000..81f85f316 --- /dev/null +++ b/apps/emqx_plugins/test/emqx_plugins_tests.erl @@ -0,0 +1,103 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2019-2021 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_plugins_tests). + +-include_lib("eunit/include/eunit.hrl"). + +ensure_configured_test() -> + try test_ensure_configured() + after emqx_plugins:put_configured([]) + end. + +test_ensure_configured() -> + ok = emqx_plugins:put_configured([]), + P1 =#{name_vsn => "p-1", enable => true}, + P2 =#{name_vsn => "p-2", enable => true}, + P3 =#{name_vsn => "p-3", enable => false}, + emqx_plugins:ensure_configured(P1, front), + emqx_plugins:ensure_configured(P2, {before, <<"p-1">>}), + emqx_plugins:ensure_configured(P3, {before, <<"p-1">>}), + ?assertEqual([P2, P3, P1], emqx_plugins:configured()), + ?assertThrow(#{error := "position_anchor_plugin_not_configured"}, + emqx_plugins:ensure_configured(P3, {before, <<"unknown-x">>})). + +read_plugin_test() -> + with_rand_install_dir( + fun(_Dir) -> + NameVsn = "bar-5", + InfoFile = emqx_plugins:info_file(NameVsn), + FakeInfo = "name=bar, rel_vsn=\"5\", rel_apps=[justname_no_vsn]," + "description=\"desc bar\"", + try + ok = write_file(InfoFile, FakeInfo), + ?assertMatch({error, #{error := "bad_rel_apps"}}, + emqx_plugins:read_plugin(NameVsn)) + after + emqx_plugins:purge(NameVsn) + end + end). + +with_rand_install_dir(F) -> + N = rand:uniform(10000000), + TmpDir = integer_to_list(N), + OriginalInstallDir = emqx_plugins:install_dir(), + ok = filelib:ensure_dir(filename:join([TmpDir, "foo"])), + ok = emqx_plugins:put_config(install_dir, TmpDir), + try + F(TmpDir) + after + file:del_dir_r(TmpDir), + ok = emqx_plugins:put_config(install_dir, OriginalInstallDir) + end. + +write_file(Path, Content) -> + ok = filelib:ensure_dir(Path), + file:write_file(Path, Content). + +%% delete package should mostly work and return ok +%% but it may fail in case the path is a directory +%% or if the file is read-only +delete_package_test() -> + with_rand_install_dir( + fun(_Dir) -> + File = emqx_plugins:pkg_file("a-1"), + ok = write_file(File, "a"), + ok = emqx_plugins:delete_package("a-1"), + %% delete again should be ok + ok = emqx_plugins:delete_package("a-1"), + Dir = File, + ok = filelib:ensure_dir(filename:join([Dir, "foo"])), + ?assertMatch({error, _}, emqx_plugins:delete_package("a-1")) + end). + +%% purge plugin's install dir should mostly work and return ok +%% but it may fail in case the dir is read-only +purge_test() -> + with_rand_install_dir( + fun(_Dir) -> + File = emqx_plugins:info_file("a-1"), + Dir = emqx_plugins:dir("a-1"), + ok = filelib:ensure_dir(File), + ?assertMatch({ok, _}, file:read_file_info(Dir)), + ?assertEqual(ok, emqx_plugins:purge("a-1")), + %% assert the dir is gone + ?assertMatch({error, enoent}, file:read_file_info(Dir)), + %% wite a file for the dir path + ok = file:write_file(Dir, "a"), + ?assertEqual(ok, emqx_plugins:purge("a-1")) + end). +