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]).
%% 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),

View File

@ -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 <Plugin>", "Load plugin"},
{"plugins unload <Plugin>", "Unload plugin"},
{"plugins reload <Plugin>", "Reload plugin"}
]).
emqx_ctl:usage(
[{"plugins <command> [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

View File

@ -24,6 +24,7 @@
, ensure_enabled/1
, ensure_enabled/2
, ensure_disabled/1
, delete_package/1
]).
-export([ ensure_started/0
@ -32,7 +33,7 @@
, ensure_stopped/1
, restart/1
, list/0
, delete_package/1
, describe/1
]).
-export([ get_config/2
@ -54,11 +55,16 @@
-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 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.
-spec ensure_installed(name_vsn()) -> ok | {error, any()}.
ensure_installed(NameVsn) ->
@ -98,7 +104,7 @@ do_ensure_installed(NameVsn) ->
-spec ensure_uninstalled(name_vsn()) -> ok | {error, any()}.
ensure_uninstalled(NameVsn) ->
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",
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.
-spec ensure_enabled(name_vsn()) -> ok | {error, any()}.
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_state(NameVsn, Position, true).
%% @doc Ensure a plugin is disabled.
-spec ensure_disabled(name_vsn()) -> ok | {error, any()}.
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(binary_to_list(NameVsn), Position, State);
@ -147,6 +155,8 @@ ensure_configured(#{name_vsn := NameVsn} = Item, Position) ->
end,
ok = put_configured(NewConfigured).
add_new_configured(Configured, no_move, Item) ->
Configured ++ [Item];
add_new_configured(Configured, front, Item) ->
[Item | Configured];
add_new_configured(Configured, rear, Item) ->
@ -232,16 +242,33 @@ restart(NameVsn) ->
-spec list() -> [plugin()].
list() ->
Pattern = filename:join([install_dir(), "*", "release.json"]),
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)).
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",
@ -295,7 +322,7 @@ plugin_status(NameVsn) ->
_ -> loaded
end;
undefined ->
not_loaded
stopped
end,
Configured = lists:filtermap(
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)]).