emqx/apps/emqx_plugins/test/emqx_plugins_SUITE.erl

830 lines
30 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2019-2024 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_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-define(EMQX_PLUGIN_APP_NAME, my_emqx_plugin).
-define(EMQX_PLUGIN_TEMPLATE_RELEASE_NAME, atom_to_list(?EMQX_PLUGIN_APP_NAME)).
-define(EMQX_PLUGIN_TEMPLATE_URL,
"https://github.com/emqx/emqx-plugin-template/releases/download/"
).
-define(EMQX_PLUGIN_TEMPLATE_VSN, "5.1.0").
-define(EMQX_PLUGIN_TEMPLATE_TAG, "5.1.0").
-define(EMQX_PLUGIN_TEMPLATES_LEGACY, [
#{
vsn => "5.0.0",
tag => "5.0.0",
release_name => "emqx_plugin_template",
app_name => emqx_plugin_template
}
]).
-define(EMQX_ELIXIR_PLUGIN_TEMPLATE_RELEASE_NAME, "elixir_plugin_template").
-define(EMQX_ELIXIR_PLUGIN_TEMPLATE_URL,
"https://github.com/emqx/emqx-elixir-plugin/releases/download/"
).
-define(EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN, "0.1.0").
-define(EMQX_ELIXIR_PLUGIN_TEMPLATE_TAG, "0.1.0-2").
-define(PACKAGE_SUFFIX, ".tar.gz").
all() ->
[
{group, copy_plugin},
{group, create_tar_copy_plugin},
emqx_common_test_helpers:all(?MODULE)
].
groups() ->
[
{copy_plugin, [sequence], [
group_t_copy_plugin_to_a_new_node,
group_t_copy_plugin_to_a_new_node_single_node,
group_t_cluster_leave
]},
{create_tar_copy_plugin, [sequence], [group_t_copy_plugin_to_a_new_node]}
].
init_per_group(copy_plugin, Config) ->
Config;
init_per_group(create_tar_copy_plugin, Config) ->
[{remove_tar, true} | Config].
end_per_group(_Group, _Config) ->
ok.
init_per_suite(Config) ->
WorkDir = emqx_cth_suite:work_dir(Config),
InstallDir = filename:join([WorkDir, "plugins"]),
Apps = emqx_cth_suite:start(
[
emqx_conf,
emqx_ctl,
{emqx_plugins, #{config => #{plugins => #{install_dir => InstallDir}}}}
],
#{work_dir => WorkDir}
),
ok = filelib:ensure_path(InstallDir),
[{suite_apps, Apps}, {install_dir, InstallDir} | Config].
end_per_suite(Config) ->
ok = emqx_cth_suite:stop(?config(suite_apps, Config)).
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}).
end_per_testcase(TestCase, Config) ->
emqx_plugins:put_configured([]),
?MODULE:TestCase({'end', Config}).
get_demo_plugin_package() ->
get_demo_plugin_package(emqx_plugins:install_dir()).
get_demo_plugin_package(
#{
release_name := ReleaseName,
git_url := GitUrl,
vsn := PluginVsn,
tag := ReleaseTag,
shdir := WorkDir
} = Opts
) ->
TargetName = lists:flatten([ReleaseName, "-", PluginVsn, ?PACKAGE_SUFFIX]),
FileURI = lists:flatten(lists:join("/", [GitUrl, ReleaseTag, TargetName])),
{ok, {_, _, PluginBin}} = httpc:request(FileURI),
Pkg = filename:join([
WorkDir,
TargetName
]),
ok = file:write_file(Pkg, PluginBin),
Opts#{package => Pkg};
get_demo_plugin_package(Dir) ->
get_demo_plugin_package(
#{
release_name => ?EMQX_PLUGIN_TEMPLATE_RELEASE_NAME,
git_url => ?EMQX_PLUGIN_TEMPLATE_URL,
vsn => ?EMQX_PLUGIN_TEMPLATE_VSN,
tag => ?EMQX_PLUGIN_TEMPLATE_TAG,
shdir => Dir
}
).
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_demo_install_start_stop_uninstall({init, Config}) ->
Opts = #{package := Package} = get_demo_plugin_package(),
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
[
{name_vsn, NameVsn},
{plugin_opts, Opts}
| Config
];
t_demo_install_start_stop_uninstall({'end', _Config}) ->
ok;
t_demo_install_start_stop_uninstall(Config) ->
NameVsn = proplists:get_value(name_vsn, Config),
#{
release_name := ReleaseName,
vsn := PluginVsn
} = proplists:get_value(plugin_opts, Config),
ok = emqx_plugins:ensure_installed(NameVsn),
%% idempotent
ok = emqx_plugins:ensure_installed(NameVsn),
{ok, Info} = emqx_plugins:describe(NameVsn),
?assertEqual([maps:without([readme], Info)], emqx_plugins:list()),
%% start
ok = emqx_plugins:ensure_started(NameVsn),
ok = assert_app_running(?EMQX_PLUGIN_APP_NAME, true),
ok = assert_app_running(map_sets, true),
%% start (idempotent)
ok = emqx_plugins:ensure_started(bin(NameVsn)),
ok = assert_app_running(?EMQX_PLUGIN_APP_NAME, true),
ok = assert_app_running(map_sets, true),
%% running app can not be un-installed
?assertMatch(
{error, _},
emqx_plugins:ensure_uninstalled(NameVsn)
),
%% stop
ok = emqx_plugins:ensure_stopped(NameVsn),
ok = assert_app_running(?EMQX_PLUGIN_APP_NAME, false),
ok = assert_app_running(map_sets, false),
%% stop (idempotent)
ok = emqx_plugins:ensure_stopped(bin(NameVsn)),
ok = assert_app_running(?EMQX_PLUGIN_APP_NAME, false),
ok = assert_app_running(map_sets, false),
%% still listed after stopped
ReleaseNameBin = list_to_binary(ReleaseName),
PluginVsnBin = list_to_binary(PluginVsn),
?assertMatch(
[
#{
<<"name">> := ReleaseNameBin,
<<"rel_vsn">> := PluginVsnBin
}
],
emqx_plugins:list()
),
ok = emqx_plugins:ensure_uninstalled(NameVsn),
?assertEqual([], emqx_plugins:list()),
ok.
%% help function 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(install_dir, Config),
InfoFile = filename:join([WorkDir, NameVsn, "release.json"]),
ok = filelib:ensure_dir(InfoFile),
ok = file:write_file(InfoFile, Content).
t_position({init, Config}) ->
#{package := Package} = get_demo_plugin_package(),
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
[{name_vsn, NameVsn} | Config];
t_position({'end', _Config}) ->
ok;
t_position(Config) ->
NameVsn = proplists:get_value(name_vsn, Config),
ok = emqx_plugins:ensure_installed(NameVsn),
ok = emqx_plugins:ensure_enabled(NameVsn),
FakeInfo =
"name=position, rel_vsn=\"2\", rel_apps=[\"position-9\"],"
"description=\"desc fake position app\"",
PosApp2 = <<"position-2">>,
ok = write_info_file(Config, PosApp2, FakeInfo),
%% fake a disabled plugin in config
ok = ensure_state(PosApp2, {before, NameVsn}, false),
ListFun = fun() ->
lists:map(
fun(
#{<<"name">> := Name, <<"rel_vsn">> := Vsn}
) ->
<<Name/binary, "-", Vsn/binary>>
end,
emqx_plugins:list()
)
end,
?assertEqual([PosApp2, list_to_binary(NameVsn)], ListFun()),
emqx_plugins:ensure_enabled(PosApp2, {behind, NameVsn}),
?assertEqual([list_to_binary(NameVsn), PosApp2], ListFun()),
ok = emqx_plugins:ensure_stopped(),
ok = emqx_plugins:ensure_disabled(NameVsn),
ok = emqx_plugins:ensure_disabled(PosApp2),
ok = emqx_plugins:ensure_uninstalled(NameVsn),
ok = emqx_plugins:ensure_uninstalled(PosApp2),
?assertEqual([], emqx_plugins:list()),
ok.
t_start_restart_and_stop({init, Config}) ->
#{package := Package} = get_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 = ensure_state(Bar2, front, false),
assert_app_running(?EMQX_PLUGIN_APP_NAME, false),
ok = emqx_plugins:ensure_started(),
assert_app_running(?EMQX_PLUGIN_APP_NAME, true),
%% fake enable bar-2
ok = 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_APP_NAME, true),
%% stop all
ok = emqx_plugins:ensure_stopped(),
assert_app_running(?EMQX_PLUGIN_APP_NAME, false),
ok = ensure_state(Bar2, rear, false),
ok = emqx_plugins:restart(NameVsn),
assert_app_running(?EMQX_PLUGIN_APP_NAME, true),
%% repeat
ok = emqx_plugins:restart(NameVsn),
assert_app_running(?EMQX_PLUGIN_APP_NAME, 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_legacy_plugins({init, Config}) ->
Config;
t_legacy_plugins({'end', _Config}) ->
ok;
t_legacy_plugins(Config) ->
lists:foreach(
fun(LegacyPlugin) ->
test_legacy_plugin(LegacyPlugin, Config)
end,
?EMQX_PLUGIN_TEMPLATES_LEGACY
).
test_legacy_plugin(#{app_name := AppName} = LegacyPlugin, _Config) ->
#{package := Package} = get_demo_plugin_package(LegacyPlugin#{
shdir => emqx_plugins:install_dir(), git_url => ?EMQX_PLUGIN_TEMPLATE_URL
}),
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
ok = emqx_plugins:ensure_installed(NameVsn),
%% start
ok = emqx_plugins:ensure_started(NameVsn),
ok = assert_app_running(AppName, true),
ok = assert_app_running(map_sets, true),
%% stop
ok = emqx_plugins:ensure_stopped(NameVsn),
ok = assert_app_running(AppName, false),
ok = assert_app_running(map_sets, false),
ok = emqx_plugins:ensure_uninstalled(NameVsn),
?assertEqual([], emqx_plugins:list()),
ok.
t_enable_disable({init, Config}) ->
#{package := Package} = get_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(install_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 with incomplete info file
%% failed install attempts should not leave behind extracted dir
t_bad_tar_gz2({init, Config}) ->
WorkDir = proplists:get_value(install_dir, Config),
NameVsn = "foo-0.2",
%% this an invalid info file content (description missing)
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),
[{tar_gz, TarGz}, {name_vsn, NameVsn} | Config];
t_bad_tar_gz2({'end', Config}) ->
NameVsn = ?config(name_vsn, Config),
ok = emqx_plugins:delete_package(NameVsn),
ok;
t_bad_tar_gz2(Config) ->
TarGz = ?config(tar_gz, Config),
NameVsn = ?config(name_vsn, Config),
?assert(filelib:is_regular(TarGz)),
%% failed to install, it also cleans up the bad content of .tar.gz file
?assertMatch({error, _}, emqx_plugins:ensure_installed(NameVsn)),
?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))),
%% but the tar.gz file is still around
?assert(filelib:is_regular(TarGz)),
ok.
%% test that we even cleanup content that doesn't match the expected name-vsn
%% pattern
t_tar_vsn_content_mismatch({init, Config}) ->
WorkDir = proplists:get_value(install_dir, Config),
NameVsn = "bad_tar-0.2",
%% this an invalid info file content
BadInfo = "name=foo, rel_vsn=\"0.2\", rel_apps=[\"foo-0.2\"], description=\"lorem ipsum\"",
ok = write_info_file(Config, "foo-0.2", BadInfo),
TarGz = filename:join([WorkDir, "bad_tar-0.2.tar.gz"]),
ok = make_tar(WorkDir, "foo-0.2", NameVsn),
file:delete(filename:join([WorkDir, "foo-0.2", "release.json"])),
[{tar_gz, TarGz}, {name_vsn, NameVsn} | Config];
t_tar_vsn_content_mismatch({'end', Config}) ->
NameVsn = ?config(name_vsn, Config),
ok = emqx_plugins:delete_package(NameVsn),
ok;
t_tar_vsn_content_mismatch(Config) ->
TarGz = ?config(tar_gz, Config),
NameVsn = ?config(name_vsn, Config),
?assert(filelib:is_regular(TarGz)),
%% failed to install, it also cleans up content of the bad .tar.gz file even
%% if in other directory
?assertMatch({error, _}, emqx_plugins:ensure_installed(NameVsn)),
?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))),
?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir("foo-0.2"))),
%% the tar.gz file is still around
?assert(filelib:is_regular(TarGz)),
ok.
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:describe(NameVsn)
),
ok = write_info_file(Config, NameVsn, "{\"bad\": \"obj\"}"),
?assertMatch(
{error, #{
error := "bad_info_file_content",
mandatory_fields := _
}},
emqx_plugins:describe(NameVsn)
),
?assertEqual([], emqx_plugins:list()),
emqx_plugins:purge(NameVsn),
ok.
t_elixir_plugin({init, Config}) ->
Opts0 =
#{
release_name => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_RELEASE_NAME,
git_url => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_URL,
vsn => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN,
tag => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_TAG,
shdir => emqx_plugins:install_dir()
},
Opts = #{package := Package} = get_demo_plugin_package(Opts0),
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
[
{name_vsn, NameVsn},
{plugin_opts, Opts}
| Config
];
t_elixir_plugin({'end', _Config}) ->
ok;
t_elixir_plugin(Config) ->
NameVsn = proplists:get_value(name_vsn, Config),
#{
release_name := ReleaseName,
vsn := PluginVsn
} = proplists:get_value(plugin_opts, 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(elixir_plugin_template, true),
ok = assert_app_running(hallux, true),
%% start (idempotent)
ok = emqx_plugins:ensure_started(bin(NameVsn)),
ok = assert_app_running(elixir_plugin_template, true),
ok = assert_app_running(hallux, true),
%% call an elixir function
1 = 'Elixir.ElixirPluginTemplate':ping(),
3 = 'Elixir.Kernel':'+'(1, 2),
%% running app can not be un-installed
?assertMatch(
{error, _},
emqx_plugins:ensure_uninstalled(NameVsn)
),
%% stop
ok = emqx_plugins:ensure_stopped(NameVsn),
ok = assert_app_running(elixir_plugin_template, false),
ok = assert_app_running(hallux, false),
%% stop (idempotent)
ok = emqx_plugins:ensure_stopped(bin(NameVsn)),
ok = assert_app_running(elixir_plugin_template, false),
ok = assert_app_running(hallux, false),
%% still listed after stopped
ReleaseNameBin = list_to_binary(ReleaseName),
PluginVsnBin = list_to_binary(PluginVsn),
?assertMatch(
[
#{
<<"name">> := ReleaseNameBin,
<<"rel_vsn">> := PluginVsnBin
}
],
emqx_plugins:list()
),
ok = emqx_plugins:ensure_uninstalled(NameVsn),
?assertEqual([], emqx_plugins:list()),
ok.
t_load_config_from_cli({init, Config}) ->
#{package := Package} = get_demo_plugin_package(),
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
[{name_vsn, NameVsn} | Config];
t_load_config_from_cli({'end', Config}) ->
NameVsn = ?config(name_vsn, Config),
ok = emqx_plugins:ensure_stopped(NameVsn),
ok = emqx_plugins:ensure_uninstalled(NameVsn),
ok;
t_load_config_from_cli(Config) when is_list(Config) ->
NameVsn = ?config(name_vsn, Config),
ok = emqx_plugins:ensure_installed(NameVsn),
?assertEqual([], emqx_plugins:configured()),
ok = emqx_plugins:ensure_enabled(NameVsn),
ok = emqx_plugins:ensure_started(NameVsn),
Params0 = unused,
?assertMatch(
{200, [#{running_status := [#{status := running}]}]},
emqx_mgmt_api_plugins:list_plugins(get, Params0)
),
%% Now we disable it via CLI loading
Conf0 = emqx_config:get([plugins]),
?assertMatch(
#{states := [#{enable := true}]},
Conf0
),
#{states := [Plugin0]} = Conf0,
Conf1 = Conf0#{states := [Plugin0#{enable := false}]},
Filename = filename:join(["/tmp", [?FUNCTION_NAME, ".hocon"]]),
ok = file:write_file(Filename, hocon_pp:do(#{plugins => Conf1}, #{})),
ok = emqx_conf_cli:conf(["load", Filename]),
Conf2 = emqx_config:get([plugins]),
?assertMatch(
#{states := [#{enable := false}]},
Conf2
),
?assertMatch(
{200, [#{running_status := [#{status := stopped}]}]},
emqx_mgmt_api_plugins:list_plugins(get, Params0)
),
%% Re-enable it via CLI loading
ok = file:write_file(Filename, hocon_pp:do(#{plugins => Conf0}, #{})),
ok = emqx_conf_cli:conf(["load", Filename]),
Conf3 = emqx_config:get([plugins]),
?assertMatch(
#{states := [#{enable := true}]},
Conf3
),
?assertMatch(
{200, [#{running_status := [#{status := running}]}]},
emqx_mgmt_api_plugins:list_plugins(get, Params0)
),
ok.
group_t_copy_plugin_to_a_new_node({init, Config}) ->
FromInstallDir = filename:join(emqx_cth_suite:work_dir(?FUNCTION_NAME, Config), from),
ok = filelib:ensure_path(FromInstallDir),
ToInstallDir = filename:join(emqx_cth_suite:work_dir(?FUNCTION_NAME, Config), to),
ok = filelib:ensure_path(ToInstallDir),
#{package := Package, release_name := PluginName} = get_demo_plugin_package(FromInstallDir),
Apps = [
emqx,
emqx_conf,
emqx_ctl,
emqx_plugins
],
[SpecCopyFrom, SpecCopyTo] =
emqx_cth_cluster:mk_nodespecs(
[
{plugins_copy_from, #{role => core, apps => Apps}},
{plugins_copy_to, #{role => core, apps => Apps}}
],
#{
work_dir => emqx_cth_suite:work_dir(?FUNCTION_NAME, Config)
}
),
[CopyFromNode] = emqx_cth_cluster:start([SpecCopyFrom#{join_to => undefined}]),
ok = rpc:call(CopyFromNode, emqx_plugins, put_config, [install_dir, FromInstallDir]),
[CopyToNode] = emqx_cth_cluster:start([SpecCopyTo#{join_to => undefined}]),
ok = rpc:call(CopyToNode, emqx_plugins, put_config, [install_dir, ToInstallDir]),
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
ok = rpc:call(CopyFromNode, emqx_plugins, ensure_installed, [NameVsn]),
ok = rpc:call(CopyFromNode, emqx_plugins, ensure_started, [NameVsn]),
ok = rpc:call(CopyFromNode, emqx_plugins, ensure_enabled, [NameVsn]),
case proplists:get_bool(remove_tar, Config) of
true ->
%% Test the case when a plugin is installed, but its original tar file is removed
%% and must be re-created
ok = file:delete(filename:join(FromInstallDir, NameVsn ++ ?PACKAGE_SUFFIX));
false ->
ok
end,
[
{from_install_dir, FromInstallDir},
{to_install_dir, ToInstallDir},
{copy_from_node, CopyFromNode},
{copy_to_node, CopyToNode},
{name_vsn, NameVsn},
{plugin_name, PluginName}
| Config
];
group_t_copy_plugin_to_a_new_node({'end', Config}) ->
CopyFromNode = ?config(copy_from_node, Config),
CopyToNode = ?config(copy_to_node, Config),
ok = emqx_cth_cluster:stop([CopyFromNode, CopyToNode]);
group_t_copy_plugin_to_a_new_node(Config) ->
CopyFromNode = proplists:get_value(copy_from_node, Config),
CopyToNode = proplists:get_value(copy_to_node, Config),
CopyToDir = proplists:get_value(to_install_dir, Config),
CopyFromPluginsState = rpc:call(CopyFromNode, emqx_plugins, get_config, [[states], []]),
NameVsn = proplists:get_value(name_vsn, Config),
PluginName = proplists:get_value(plugin_name, Config),
PluginApp = list_to_atom(PluginName),
?assertMatch([#{enable := true, name_vsn := NameVsn}], CopyFromPluginsState),
?assert(
proplists:is_defined(
PluginApp,
rpc:call(CopyFromNode, application, which_applications, [])
)
),
?assertEqual([], filelib:wildcard(filename:join(CopyToDir, "**"))),
%% Check that a new node doesn't have this plugin before it joins the cluster
?assertEqual([], rpc:call(CopyToNode, emqx_conf, get, [[plugins, states], []])),
?assertMatch({error, _}, rpc:call(CopyToNode, emqx_plugins, describe, [NameVsn])),
?assertNot(
proplists:is_defined(
PluginApp,
rpc:call(CopyToNode, application, which_applications, [])
)
),
ok = rpc:call(CopyToNode, ekka, join, [CopyFromNode]),
%% Mimic cluster-override conf copying
ok = rpc:call(CopyToNode, emqx_plugins, put_config, [[states], CopyFromPluginsState]),
%% Plugin copying is triggered upon app restart on a new node.
%% This is similar to emqx_conf, which copies cluster-override conf upon start,
%% see: emqx_conf_app:init_conf/0
ok = rpc:call(CopyToNode, application, stop, [emqx_plugins]),
{ok, _} = rpc:call(CopyToNode, application, ensure_all_started, [emqx_plugins]),
?assertMatch(
{ok, #{running_status := running, config_status := enabled}},
rpc:call(CopyToNode, emqx_plugins, describe, [NameVsn])
).
%% checks that we can start a cluster with a lone node.
group_t_copy_plugin_to_a_new_node_single_node({init, Config}) ->
ToInstallDir = emqx_cth_suite:work_dir(?FUNCTION_NAME, Config),
file:del_dir_r(ToInstallDir),
ok = filelib:ensure_path(ToInstallDir),
#{package := Package, release_name := PluginName} = get_demo_plugin_package(ToInstallDir),
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
Apps = [
emqx,
emqx_conf,
emqx_ctl,
{emqx_plugins, #{
config => #{
plugins => #{
install_dir => ToInstallDir,
states => [#{name_vsn => NameVsn, enable => true}]
}
}
}}
],
[CopyToNode] = emqx_cth_cluster:start(
[{plugins_copy_to, #{role => core, apps => Apps}}],
#{work_dir => emqx_cth_suite:work_dir(?FUNCTION_NAME, Config)}
),
[
{to_install_dir, ToInstallDir},
{copy_to_node, CopyToNode},
{name_vsn, NameVsn},
{plugin_name, PluginName}
| Config
];
group_t_copy_plugin_to_a_new_node_single_node({'end', Config}) ->
CopyToNode = proplists:get_value(copy_to_node, Config),
ok = emqx_cth_cluster:stop([CopyToNode]);
group_t_copy_plugin_to_a_new_node_single_node(Config) ->
CopyToNode = ?config(copy_to_node, Config),
ToInstallDir = ?config(to_install_dir, Config),
NameVsn = proplists:get_value(name_vsn, Config),
%% Start the node for the first time. The plugin should start
%% successfully even if it's not extracted yet. Simply starting
%% the node would crash if not working properly.
ct:pal("~p config:\n ~p", [
CopyToNode, erpc:call(CopyToNode, emqx_plugins, get_config, [[], #{}])
]),
ct:pal("~p install_dir:\n ~p", [
CopyToNode, erpc:call(CopyToNode, file, list_dir, [ToInstallDir])
]),
?assertMatch(
{ok, #{running_status := running, config_status := enabled}},
rpc:call(CopyToNode, emqx_plugins, describe, [NameVsn])
),
ok.
group_t_cluster_leave({init, Config}) ->
Specs = emqx_cth_cluster:mk_nodespecs(
[
{group_t_cluster_leave1, #{role => core, apps => [emqx, emqx_conf, emqx_ctl]}},
{group_t_cluster_leave2, #{role => core, apps => [emqx, emqx_conf, emqx_ctl]}}
],
#{work_dir => emqx_cth_suite:work_dir(?FUNCTION_NAME, Config)}
),
Nodes = emqx_cth_cluster:start(Specs),
InstallRelDir = "plugins_copy_to",
InstallDirs = [filename:join(WD, InstallRelDir) || #{work_dir := WD} <- Specs],
ok = lists:foreach(fun filelib:ensure_path/1, InstallDirs),
#{package := Package, release_name := PluginName} = get_demo_plugin_package(hd(InstallDirs)),
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
[{ok, _}, {ok, _}] = erpc:multicall(Nodes, emqx_cth_suite, start_app, [
emqx_plugins,
#{
config => #{
plugins => #{
install_dir => InstallRelDir,
states => [#{name_vsn => NameVsn, enable => true}]
}
}
}
]),
[
{nodes, Nodes},
{name_vsn, NameVsn},
{plugin_name, PluginName}
| Config
];
group_t_cluster_leave({'end', Config}) ->
Nodes = ?config(nodes, Config),
ok = emqx_cth_cluster:stop(Nodes);
group_t_cluster_leave(Config) ->
[N1, N2] = ?config(nodes, Config),
NameVsn = proplists:get_value(name_vsn, Config),
ok = erpc:call(N1, emqx_plugins, ensure_installed, [NameVsn]),
ok = erpc:call(N1, emqx_plugins, ensure_started, [NameVsn]),
ok = erpc:call(N1, emqx_plugins, ensure_enabled, [NameVsn]),
Params = unused,
%% 2 nodes running
?assertMatch(
{200, [#{running_status := [#{status := running}, #{status := running}]}]},
erpc:call(N1, emqx_mgmt_api_plugins, list_plugins, [get, Params])
),
?assertMatch(
{200, [#{running_status := [#{status := running}, #{status := running}]}]},
erpc:call(N2, emqx_mgmt_api_plugins, list_plugins, [get, Params])
),
%% Now, one node leaves the cluster.
ok = erpc:call(N2, ekka, leave, []),
%% Each node will no longer ask the plugin status to the other.
?assertMatch(
{200, [#{running_status := [#{node := N1, status := running}]}]},
erpc:call(N1, emqx_mgmt_api_plugins, list_plugins, [get, Params])
),
?assertMatch(
{200, [#{running_status := [#{node := N2, status := running}]}]},
erpc:call(N2, emqx_mgmt_api_plugins, list_plugins, [get, Params])
),
ok.
make_tar(Cwd, NameWithVsn) ->
make_tar(Cwd, NameWithVsn, NameWithVsn).
make_tar(Cwd, NameWithVsn, TarfileVsn) ->
{ok, OriginalCwd} = file:get_cwd(),
ok = file:set_cwd(Cwd),
try
Files = filelib:wildcard(NameWithVsn ++ "/**"),
TarFile = TarfileVsn ++ ".tar.gz",
ok = erl_tar:create(TarFile, Files, [compressed])
after
file:set_cwd(OriginalCwd)
end.
ensure_state(NameVsn, Position, Enabled) ->
%% NOTE: this is an internal function that is (legacy) exported in test builds only...
emqx_plugins:ensure_state(NameVsn, Position, Enabled, _ConfLocation = local).