refactor(plugins): new CLI for plugins

This commit is contained in:
Zaiming (Stone) Shi 2021-12-16 20:11:14 +01:00
parent 47661042b9
commit 3a7924d0fd
4 changed files with 187 additions and 55 deletions

View File

@ -31,6 +31,9 @@
-export([format/2]). -export([format/2]).
%% For CLI outputs
-export([best_effort_json/1]).
-ifdef(TEST). -ifdef(TEST).
-include_lib("proper/include/proper.hrl"). -include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
@ -51,6 +54,16 @@
-define(IS_STRING(String), (is_list(String) orelse is_binary(String))). -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(). -spec format(logger:log_event(), config()) -> iodata().
format(#{level := Level, msg := Msg, meta := Meta}, Config0) when is_map(Config0) -> format(#{level := Level, msg := Msg, meta := Meta}, Config0) when is_map(Config0) ->
Config = add_default_config(Config0), Config = add_default_config(Config0),

View File

@ -225,46 +225,50 @@ if_valid_qos(QoS, Fun) ->
end. end.
plugins(["list"]) -> plugins(["list"]) ->
lists:foreach(fun print/1, emqx_plugins:list()); emqx_plugins_cli:list(fun emqx_ctl:print/2);
plugins(["describe", NameVsn]) ->
plugins(["load", Name]) -> emqx_plugins_cli:describe(NameVsn, fun emqx_ctl:print/2);
case emqx_plugins:load(list_to_atom(Name)) of plugins(["install", NameVsn]) ->
ok -> emqx_plugins_cli:ensure_installed(NameVsn, fun emqx_ctl:print/2);
emqx_ctl:print("Plugin ~ts loaded successfully.~n", [Name]); plugins(["uninstall", NameVsn])->
{error, Reason} -> emqx_plugins_cli:ensure_uninstalled(NameVsn, fun emqx_ctl:print/2);
emqx_ctl:print("Load plugin ~ts error: ~p.~n", [Name, Reason]) plugins(["start", NameVsn]) ->
end; emqx_plugins_cli:ensure_started(NameVsn, fun emqx_ctl:print/2);
plugins(["stop", NameVsn]) ->
plugins(["unload", "emqx_management"])-> emqx_plugins_cli:ensure_stopped(NameVsn, fun emqx_ctl:print/2);
emqx_ctl:print("Plugin emqx_management can not be unloaded.~n"); plugins(["restart", NameVsn]) ->
emqx_plugins_cli:restart(NameVsn, fun emqx_ctl:print/2);
plugins(["unload", Name]) -> plugins(["disable", NameVsn]) ->
case emqx_plugins:unload(list_to_atom(Name)) of emqx_plugins_cli:ensure_disabled(NameVsn, fun emqx_ctl:print/2);
ok -> plugins(["enable", NameVsn]) ->
emqx_ctl:print("Plugin ~ts unloaded successfully.~n", [Name]); emqx_plugins_cli:ensure_enabled(NameVsn, no_move, fun emqx_ctl:print/2);
{error, Reason} -> plugins(["enable", NameVsn, "front"]) ->
emqx_ctl:print("Unload plugin ~ts error: ~p.~n", [Name, Reason]) emqx_plugins_cli:ensure_enabled(NameVsn, front, fun emqx_ctl:print/2);
end; plugins(["enable", NameVsn, "rear"]) ->
emqx_plugins_cli:ensure_enabled(NameVsn, rear, fun emqx_ctl:print/2);
plugins(["reload", Name]) -> plugins(["enable", NameVsn, "before", Other]) ->
try list_to_existing_atom(Name) of emqx_plugins_cli:ensure_enabled(NameVsn, {before, Other}, fun emqx_ctl:print/2);
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;
plugins(_) -> plugins(_) ->
emqx_ctl:usage([{"plugins list", "Show loaded plugins"}, emqx_ctl:usage(
{"plugins load <Plugin>", "Load plugin"}, [{"plugins <command> [Name-Vsn]", "e.g. 'start emqx_plugin_template-5.0-rc.1'"},
{"plugins unload <Plugin>", "Unload plugin"}, {"plugins list", "List all installed plugins"},
{"plugins reload <Plugin>", "Reload plugin"} {"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"}
]). ]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -24,6 +24,7 @@
, ensure_enabled/1 , ensure_enabled/1
, ensure_enabled/2 , ensure_enabled/2
, ensure_disabled/1 , ensure_disabled/1
, delete_package/1
]). ]).
-export([ ensure_started/0 -export([ ensure_started/0
@ -32,7 +33,7 @@
, ensure_stopped/1 , ensure_stopped/1
, restart/1 , restart/1
, list/0 , list/0
, delete_package/1 , describe/1
]). ]).
-export([ get_config/2 -export([ get_config/2
@ -54,11 +55,16 @@
-type name_vsn() :: binary() | string(). %% "my_plugin-0.1.0" -type name_vsn() :: binary() | string(). %% "my_plugin-0.1.0"
-type plugin() :: map(). %% the parse result of the JSON info file -type plugin() :: map(). %% the parse result of the JSON info file
-type position() :: no_move | front | rear | {before, name_vsn()}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% APIs %% APIs
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% @doc Describe a plugin.
-spec describe(name_vsn()) -> {ok, plugin()} | {error, any()}.
describe(NameVsn) -> read_plugin(NameVsn).
%% @doc Install a .tar.gz package placed in install_dir. %% @doc Install a .tar.gz package placed in install_dir.
-spec ensure_installed(name_vsn()) -> ok | {error, any()}. -spec ensure_installed(name_vsn()) -> ok | {error, any()}.
ensure_installed(NameVsn) -> ensure_installed(NameVsn) ->
@ -98,7 +104,7 @@ do_ensure_installed(NameVsn) ->
-spec ensure_uninstalled(name_vsn()) -> ok | {error, any()}. -spec ensure_uninstalled(name_vsn()) -> ok | {error, any()}.
ensure_uninstalled(NameVsn) -> ensure_uninstalled(NameVsn) ->
case read_plugin(NameVsn) of case read_plugin(NameVsn) of
{ok, #{running_status := RunningSt}} when RunningSt =/= not_loaded -> {ok, #{running_status := RunningSt}} when RunningSt =/= stopped ->
{error, #{reason => "bad_plugin_running_status", {error, #{reason => "bad_plugin_running_status",
hint => "stop_the_plugin_first" hint => "stop_the_plugin_first"
}}; }};
@ -113,15 +119,17 @@ ensure_uninstalled(NameVsn) ->
%% @doc Ensure a plugin is enabled to the end of the plugins list. %% @doc Ensure a plugin is enabled to the end of the plugins list.
-spec ensure_enabled(name_vsn()) -> ok | {error, any()}. -spec ensure_enabled(name_vsn()) -> ok | {error, any()}.
ensure_enabled(NameVsn) -> ensure_enabled(NameVsn) ->
ensure_enabled(NameVsn, rear). 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_enabled(NameVsn, Position) ->
ensure_state(NameVsn, Position, true). ensure_state(NameVsn, Position, true).
%% @doc Ensure a plugin is disabled. %% @doc Ensure a plugin is disabled.
-spec ensure_disabled(name_vsn()) -> ok | {error, any()}. -spec ensure_disabled(name_vsn()) -> ok | {error, any()}.
ensure_disabled(NameVsn) -> ensure_disabled(NameVsn) ->
ensure_state(NameVsn, rear, false). ensure_state(NameVsn, no_move, false).
ensure_state(NameVsn, Position, State) when is_binary(NameVsn) -> ensure_state(NameVsn, Position, State) when is_binary(NameVsn) ->
ensure_state(binary_to_list(NameVsn), Position, State); ensure_state(binary_to_list(NameVsn), Position, State);
@ -147,6 +155,8 @@ ensure_configured(#{name_vsn := NameVsn} = Item, Position) ->
end, end,
ok = put_configured(NewConfigured). ok = put_configured(NewConfigured).
add_new_configured(Configured, no_move, Item) ->
Configured ++ [Item];
add_new_configured(Configured, front, Item) -> add_new_configured(Configured, front, Item) ->
[Item | Configured]; [Item | Configured];
add_new_configured(Configured, rear, Item) -> add_new_configured(Configured, rear, Item) ->
@ -232,7 +242,7 @@ restart(NameVsn) ->
-spec list() -> [plugin()]. -spec list() -> [plugin()].
list() -> list() ->
Pattern = filename:join([install_dir(), "*", "release.json"]), Pattern = filename:join([install_dir(), "*", "release.json"]),
lists:filtermap( All = lists:filtermap(
fun(JsonFile) -> fun(JsonFile) ->
case read_plugin({file, JsonFile}) of case read_plugin({file, JsonFile}) of
{ok, Info} -> {ok, Info} ->
@ -241,7 +251,24 @@ list() ->
?SLOG(warning, Reason), ?SLOG(warning, Reason),
false false
end end
end, filelib:wildcard(Pattern)). 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) -> do_ensure_started(NameVsn) ->
tryit("start_plugins", tryit("start_plugins",
@ -295,7 +322,7 @@ plugin_status(NameVsn) ->
_ -> loaded _ -> loaded
end; end;
undefined -> undefined ->
not_loaded stopped
end, end,
Configured = lists:filtermap( Configured = lists:filtermap(
fun(#{name_vsn := Nv, enable := St}) -> fun(#{name_vsn := Nv, enable := St}) ->

View File

@ -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)]).