瀏覽代碼

refactor(plugins): new CLI for plugins

Zaiming (Stone) Shi 4 年之前
父節點
當前提交
3a7924d0fd

+ 13 - 0
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),

+ 44 - 40
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 <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

+ 42 - 15
apps/emqx_plugins/src/emqx_plugins.erl

@@ -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}) ->

+ 88 - 0
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)]).