fix(emqx_plugins): allow loading conf for plugin app dir

Prior to this change, plugin config files are only allowed
to be placed in the collective config dir etc/plugins.
In order to support external plugin's drop-in deployment,
this commit made emqx_plugins module to read conf file
in application's etc dir
This commit is contained in:
Zaiming Shi 2021-05-20 13:31:10 +02:00
parent 6436217e07
commit faecde9ce1
4 changed files with 75 additions and 63 deletions

View File

@ -61,7 +61,7 @@ init() ->
%% @doc Load all plugins when the broker started. %% @doc Load all plugins when the broker started.
-spec(load() -> ok | ignore | {error, term()}). -spec(load() -> ok | ignore | {error, term()}).
load() -> load() ->
load_expand_plugins(), ok = load_ext_plugins(emqx:get_env(expand_plugins_dir)),
case emqx:get_env(plugins_loaded_file) of case emqx:get_env(plugins_loaded_file) of
undefined -> ignore; %% No plugins available undefined -> ignore; %% No plugins available
File -> File ->
@ -148,46 +148,61 @@ init_config(CfgFile) ->
[application:set_env(App, Par, Val) || {Par, Val} <- Envs] [application:set_env(App, Par, Val) || {Par, Val} <- Envs]
end, AppsEnv). end, AppsEnv).
load_expand_plugins() -> %% load external plugins which are placed in etc/plugins dir
case emqx:get_env(expand_plugins_dir) of load_ext_plugins(undefined) -> ok;
undefined -> ok; load_ext_plugins(Dir) ->
ExpandPluginsDir -> lists:foreach(
Plugins = filelib:wildcard("*", ExpandPluginsDir), fun(Plugin) ->
lists:foreach(fun(Plugin) -> PluginDir = filename:join(Dir, Plugin),
PluginDir = filename:join(ExpandPluginsDir, Plugin),
case filelib:is_dir(PluginDir) of case filelib:is_dir(PluginDir) of
true -> load_expand_plugin(PluginDir); true -> load_ext_plugin(PluginDir);
false -> ok false -> ok
end end
end, Plugins) end, filelib:wildcard("*", Dir)).
end.
load_expand_plugin(PluginDir) -> load_ext_plugin(PluginDir) ->
init_expand_plugin_config(PluginDir), ?LOG(debug, "loading_extra_plugin: ~s", [PluginDir]),
Ebin = filename:join([PluginDir, "ebin"]), Ebin = filename:join([PluginDir, "ebin"]),
AppFile = filename:join([Ebin, "*.app"]),
AppName = case filelib:wildcard(AppFile) of
[App] ->
list_to_atom(filename:basename(App, ".app"));
[] ->
?LOG(alert, "plugin_app_file_not_found: ~s", [AppFile]),
error({plugin_app_file_not_found, AppFile})
end,
ok = load_plugin_app(AppName, Ebin),
ok = load_plugin_conf(AppName, PluginDir).
load_plugin_app(AppName, Ebin) ->
_ = code:add_patha(Ebin), _ = code:add_patha(Ebin),
Modules = filelib:wildcard(filename:join([Ebin, "*.beam"])), Modules = filelib:wildcard(filename:join([Ebin, "*.beam"])),
lists:foreach(fun(Mod) -> lists:foreach(
Module = list_to_atom(filename:basename(Mod, ".beam")), fun(BeamFile) ->
code:load_file(Module) Module = list_to_atom(filename:basename(BeamFile, ".beam")),
end, Modules), case code:ensure_loaded(Module) of
case filelib:wildcard(Ebin ++ "/*.app") of {module, Module} -> ok;
[App|_] -> application:load(list_to_atom(filename:basename(App, ".app"))); {error, Reason} -> error({failed_to_load_plugin_beam, BeamFile, Reason})
_ -> ?LOG(alert, "Plugin not found."), end
{error, load_app_fail} end, Modules),
case application:load(AppName) of
ok -> ok;
{error, {already_loaded, _}} -> ok
end. end.
init_expand_plugin_config(PluginDir) -> load_plugin_conf(AppName, PluginDir) ->
Priv = PluginDir ++ "/priv", Priv = filename:join([PluginDir, "priv"]),
Etc = PluginDir ++ "/etc", Etc = filename:join([PluginDir, "etc"]),
Schema = filelib:wildcard(Priv ++ "/*.schema"), Schema = filelib:wildcard(filename:join([Priv, "*.schema"])),
Conf = case filelib:wildcard(Etc ++ "/*.conf") of ConfFile = filename:join([Etc, atom_to_list(AppName) ++ ".conf"]),
[] -> []; Conf = case filelib:is_file(ConfFile) of
[Conf1] -> cuttlefish_conf:file(Conf1) true -> cuttlefish_conf:file(ConfFile);
end, false -> error({conf_file_not_found, ConfFile})
end,
?LOG(debug, "loading_extra_plugin_config conf=~s, schema=~s", [ConfFile, Schema]),
AppsEnv = cuttlefish_generator:map(cuttlefish_schema:files(Schema), Conf), AppsEnv = cuttlefish_generator:map(cuttlefish_schema:files(Schema), Conf),
lists:foreach(fun({AppName, Envs}) -> lists:foreach(fun({AppName1, Envs}) ->
[application:set_env(AppName, Par, Val) || {Par, Val} <- Envs] [application:set_env(AppName1, Par, Val) || {Par, Val} <- Envs]
end, AppsEnv). end, AppsEnv).
ensure_file(File) -> ensure_file(File) ->
@ -223,19 +238,31 @@ load_plugins(Names, Persistent) ->
generate_configs(App) -> generate_configs(App) ->
ConfigFile = filename:join([emqx:get_env(plugins_etc_dir), App]) ++ ".config", ConfigFile = filename:join([emqx:get_env(plugins_etc_dir), App]) ++ ".config",
ConfFile = filename:join([emqx:get_env(plugins_etc_dir), App]) ++ ".conf", case filelib:is_file(ConfigFile) of
SchemaFile = filename:join([code:priv_dir(App), App]) ++ ".schema", true ->
case {filelib:is_file(ConfigFile), filelib:is_file(ConfFile) andalso filelib:is_file(SchemaFile)} of
{true, _} ->
{ok, [Configs]} = file:consult(ConfigFile), {ok, [Configs]} = file:consult(ConfigFile),
Configs; Configs;
{_, true} -> false ->
do_generate_configs(App)
end.
do_generate_configs(App) ->
Name1 = filename:join([emqx:get_env(plugins_etc_dir), App]) ++ ".conf",
Name2 = filename:join([code:lib_dir(App), "etc", App]) ++ ".conf",
ConfFile = case {filelib:is_file(Name1), filelib:is_file(Name2)} of
{true, _} -> Name1;
{false, true} -> Name2;
{false, false} -> error({config_not_found, [Name1, Name2]})
end,
SchemaFile = filename:join([code:priv_dir(App), App]) ++ ".schema",
case filelib:is_file(SchemaFile) of
true ->
Schema = cuttlefish_schema:files([SchemaFile]), Schema = cuttlefish_schema:files([SchemaFile]),
Conf = cuttlefish_conf:file(ConfFile), Conf = cuttlefish_conf:file(ConfFile),
LogFun = fun(Key, Value) -> ?LOG(info, "~s = ~p", [string:join(Key, "."), Value]) end, LogFun = fun(Key, Value) -> ?LOG(info, "~s = ~p", [string:join(Key, "."), Value]) end,
cuttlefish_generator:map(Schema, Conf, undefined, LogFun); cuttlefish_generator:map(Schema, Conf, undefined, LogFun);
{false, false} -> false ->
error({config_not_found, {ConfigFile, ConfFile, SchemaFile}}) error({schema_not_found, SchemaFile})
end. end.
apply_configs([]) -> apply_configs([]) ->

View File

@ -30,24 +30,20 @@ init_per_suite(Config) ->
DataPath = proplists:get_value(data_dir, Config), DataPath = proplists:get_value(data_dir, Config),
AppPath = filename:join([DataPath, "emqx_mini_plugin"]), AppPath = filename:join([DataPath, "emqx_mini_plugin"]),
Cmd = lists:flatten(io_lib:format("cd ~s && make && cp -r etc _build/default/lib/emqx_mini_plugin/", [AppPath])), Cmd = lists:flatten(io_lib:format("cd ~s && make", [AppPath])),
ct:pal("Executing ~s~n", [Cmd]), ct:pal("Executing ~s~n", [Cmd]),
ct:pal("~n ~s~n", [os:cmd(Cmd)]), ct:pal("~n ~s~n", [os:cmd(Cmd)]),
code:add_path(filename:join([AppPath, "_build", "default", "lib", "emqx_mini_plugin", "ebin"])),
put(loaded_file, filename:join([DataPath, "loaded_plugins"])), put(loaded_file, filename:join([DataPath, "loaded_plugins"])),
emqx_ct_helpers:boot_modules([]), emqx_ct_helpers:boot_modules([]),
emqx_ct_helpers:start_apps([], fun set_sepecial_cfg/1), emqx_ct_helpers:start_apps([], fun(_) -> set_sepecial_cfg(DataPath) end),
Config. Config.
set_sepecial_cfg(_) -> set_sepecial_cfg(PluginsDir) ->
ExpandPath = filename:dirname(code:lib_dir(emqx_mini_plugin)),
application:set_env(emqx, plugins_loaded_file, get(loaded_file)), application:set_env(emqx, plugins_loaded_file, get(loaded_file)),
application:set_env(emqx, expand_plugins_dir, ExpandPath), application:set_env(emqx, expand_plugins_dir, PluginsDir),
ok. ok.
end_per_suite(_Config) -> end_per_suite(_Config) ->
@ -58,7 +54,6 @@ t_load(_) ->
?assertEqual(ok, emqx_plugins:unload()), ?assertEqual(ok, emqx_plugins:unload()),
?assertEqual({error, not_found}, emqx_plugins:load(not_existed_plugin)), ?assertEqual({error, not_found}, emqx_plugins:load(not_existed_plugin)),
?assertEqual({error, parse_config_file_failed}, emqx_plugins:load(emqx_mini_plugin)),
?assertEqual({error, not_started}, emqx_plugins:unload(emqx_mini_plugin)), ?assertEqual({error, not_started}, emqx_plugins:unload(emqx_mini_plugin)),
application:set_env(emqx, expand_plugins_dir, undefined), application:set_env(emqx, expand_plugins_dir, undefined),
@ -75,8 +70,9 @@ t_init_config(_) ->
file:delete(ConfFile), file:delete(ConfFile),
?assertEqual({ok,test}, application:get_env(emqx_mini_plugin, mininame)). ?assertEqual({ok,test}, application:get_env(emqx_mini_plugin, mininame)).
t_load_expand_plugin(_) -> t_load_ext_plugin(_) ->
?assertEqual({error, load_app_fail}, emqx_plugins:load_expand_plugin("./not_existed_path/")). ?assertError({plugin_app_file_not_found, _},
emqx_plugins:load_ext_plugin("./not_existed_path/")).
t_list(_) -> t_list(_) ->
?assertMatch([{plugin, _, _, _, _, _, _, _} | _ ], emqx_plugins:list()). ?assertMatch([{plugin, _, _, _, _, _, _, _} | _ ], emqx_plugins:list()).

View File

@ -8,6 +8,7 @@ all: compile
compile: compile:
$(REBAR) compile $(REBAR) compile
cp -r _build/default/lib/emqx_mini_plugin/ebin ./
clean: distclean clean: distclean
@ -22,14 +23,4 @@ xref:
distclean: distclean:
@rm -rf _build @rm -rf _build
@rm -f data/app.*.config data/vm.*.args rebar.lock @rm -f ebin/ data/app.*.config data/vm.*.args rebar.lock
CUTTLEFISH_SCRIPT = _build/default/lib/cuttlefish/cuttlefish
$(CUTTLEFISH_SCRIPT):
@${REBAR} get-deps
@if [ ! -f cuttlefish ]; then make -C _build/default/lib/cuttlefish; fi
app.config: $(CUTTLEFISH_SCRIPT) etc/emqx_mini_plugin.conf
$(verbose) $(CUTTLEFISH_SCRIPT) -l info -e etc/ -c etc/emqx_mini_plugin.conf -i priv/emqx_mini_plugin.schema -d data

View File

@ -1,5 +1,4 @@
{deps, {deps, []}.
[]}.
{edoc_opts, [{preprocess, true}]}. {edoc_opts, [{preprocess, true}]}.
{erl_opts, [warn_unused_vars, {erl_opts, [warn_unused_vars,
@ -19,7 +18,6 @@
{profiles, {profiles,
[{test, [ [{test, [
{deps, [ {emqx_ct_helper, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.1.4"}}} {deps, [ {emqx_ct_helper, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.1.4"}}}
, {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v3.0.0"}}}
]} ]}
]} ]}
]}. ]}.