emqx_plugins_SUITE.erl 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2019-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
  3. %%
  4. %% Licensed under the Apache License, Version 2.0 (the "License");
  5. %% you may not use this file except in compliance with the License.
  6. %% You may obtain a copy of the License at
  7. %%
  8. %% http://www.apache.org/licenses/LICENSE-2.0
  9. %%
  10. %% Unless required by applicable law or agreed to in writing, software
  11. %% distributed under the License is distributed on an "AS IS" BASIS,
  12. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. %% See the License for the specific language governing permissions and
  14. %% limitations under the License.
  15. %%--------------------------------------------------------------------
  16. -module(emqx_plugins_SUITE).
  17. -compile(export_all).
  18. -compile(nowarn_export_all).
  19. -include_lib("eunit/include/eunit.hrl").
  20. -include_lib("common_test/include/ct.hrl").
  21. -define(EMQX_PLUGIN_TEMPLATE_RELEASE_NAME, "emqx_plugin_template").
  22. -define(EMQX_PLUGIN_TEMPLATE_URL,
  23. "https://github.com/emqx/emqx-plugin-template/releases/download/"
  24. ).
  25. -define(EMQX_PLUGIN_TEMPLATE_VSN, "5.0.0").
  26. -define(EMQX_PLUGIN_TEMPLATE_TAG, "5.0.0").
  27. -define(EMQX_ELIXIR_PLUGIN_TEMPLATE_RELEASE_NAME, "elixir_plugin_template").
  28. -define(EMQX_ELIXIR_PLUGIN_TEMPLATE_URL,
  29. "https://github.com/emqx/emqx-elixir-plugin/releases/download/"
  30. ).
  31. -define(EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN, "0.1.0").
  32. -define(EMQX_ELIXIR_PLUGIN_TEMPLATE_TAG, "0.1.0-2").
  33. -define(PACKAGE_SUFFIX, ".tar.gz").
  34. all() ->
  35. [
  36. {group, copy_plugin},
  37. {group, create_tar_copy_plugin},
  38. emqx_common_test_helpers:all(?MODULE)
  39. ].
  40. groups() ->
  41. [
  42. {copy_plugin, [sequence], [group_t_copy_plugin_to_a_new_node]},
  43. {create_tar_copy_plugin, [sequence], [group_t_copy_plugin_to_a_new_node]}
  44. ].
  45. init_per_group(copy_plugin, Config) ->
  46. Config;
  47. init_per_group(create_tar_copy_plugin, Config) ->
  48. [{remove_tar, true} | Config].
  49. end_per_group(_Group, _Config) ->
  50. ok.
  51. init_per_suite(Config) ->
  52. WorkDir = proplists:get_value(data_dir, Config),
  53. filelib:ensure_path(WorkDir),
  54. OrigInstallDir = emqx_plugins:get_config(install_dir, undefined),
  55. emqx_common_test_helpers:start_apps([emqx_conf]),
  56. emqx_plugins:put_config(install_dir, WorkDir),
  57. [{orig_install_dir, OrigInstallDir} | Config].
  58. end_per_suite(Config) ->
  59. emqx_common_test_helpers:boot_modules(all),
  60. emqx_config:erase(plugins),
  61. %% restore config
  62. case proplists:get_value(orig_install_dir, Config) of
  63. undefined -> ok;
  64. OrigInstallDir -> emqx_plugins:put_config(install_dir, OrigInstallDir)
  65. end,
  66. emqx_common_test_helpers:stop_apps([emqx_conf]),
  67. ok.
  68. init_per_testcase(TestCase, Config) ->
  69. emqx_plugins:put_configured([]),
  70. lists:foreach(
  71. fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) ->
  72. emqx_plugins:purge(bin([Name, "-", Vsn]))
  73. end,
  74. emqx_plugins:list()
  75. ),
  76. ?MODULE:TestCase({init, Config}).
  77. end_per_testcase(TestCase, Config) ->
  78. emqx_plugins:put_configured([]),
  79. ?MODULE:TestCase({'end', Config}).
  80. get_demo_plugin_package() ->
  81. get_demo_plugin_package(emqx_plugins:install_dir()).
  82. get_demo_plugin_package(
  83. #{
  84. release_name := ReleaseName,
  85. git_url := GitUrl,
  86. vsn := PluginVsn,
  87. tag := ReleaseTag,
  88. shdir := WorkDir
  89. } = Opts
  90. ) ->
  91. TargetName = lists:flatten([ReleaseName, "-", PluginVsn, ?PACKAGE_SUFFIX]),
  92. FileURI = lists:flatten(lists:join("/", [GitUrl, ReleaseTag, TargetName])),
  93. {ok, {_, _, PluginBin}} = httpc:request(FileURI),
  94. Pkg = filename:join([
  95. WorkDir,
  96. TargetName
  97. ]),
  98. ok = file:write_file(Pkg, PluginBin),
  99. Opts#{package => Pkg};
  100. get_demo_plugin_package(Dir) ->
  101. get_demo_plugin_package(
  102. #{
  103. release_name => ?EMQX_PLUGIN_TEMPLATE_RELEASE_NAME,
  104. git_url => ?EMQX_PLUGIN_TEMPLATE_URL,
  105. vsn => ?EMQX_PLUGIN_TEMPLATE_VSN,
  106. tag => ?EMQX_PLUGIN_TEMPLATE_TAG,
  107. shdir => Dir
  108. }
  109. ).
  110. bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
  111. bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8);
  112. bin(B) when is_binary(B) -> B.
  113. t_demo_install_start_stop_uninstall({init, Config}) ->
  114. Opts = #{package := Package} = get_demo_plugin_package(),
  115. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  116. [
  117. {name_vsn, NameVsn},
  118. {plugin_opts, Opts}
  119. | Config
  120. ];
  121. t_demo_install_start_stop_uninstall({'end', _Config}) ->
  122. ok;
  123. t_demo_install_start_stop_uninstall(Config) ->
  124. NameVsn = proplists:get_value(name_vsn, Config),
  125. #{
  126. release_name := ReleaseName,
  127. vsn := PluginVsn
  128. } = proplists:get_value(plugin_opts, Config),
  129. ok = emqx_plugins:ensure_installed(NameVsn),
  130. %% idempotent
  131. ok = emqx_plugins:ensure_installed(NameVsn),
  132. {ok, Info} = emqx_plugins:describe(NameVsn),
  133. ?assertEqual([maps:without([readme], Info)], emqx_plugins:list()),
  134. %% start
  135. ok = emqx_plugins:ensure_started(NameVsn),
  136. ok = assert_app_running(emqx_plugin_template, true),
  137. ok = assert_app_running(map_sets, true),
  138. %% start (idempotent)
  139. ok = emqx_plugins:ensure_started(bin(NameVsn)),
  140. ok = assert_app_running(emqx_plugin_template, true),
  141. ok = assert_app_running(map_sets, true),
  142. %% running app can not be un-installed
  143. ?assertMatch(
  144. {error, _},
  145. emqx_plugins:ensure_uninstalled(NameVsn)
  146. ),
  147. %% stop
  148. ok = emqx_plugins:ensure_stopped(NameVsn),
  149. ok = assert_app_running(emqx_plugin_template, false),
  150. ok = assert_app_running(map_sets, false),
  151. %% stop (idempotent)
  152. ok = emqx_plugins:ensure_stopped(bin(NameVsn)),
  153. ok = assert_app_running(emqx_plugin_template, false),
  154. ok = assert_app_running(map_sets, false),
  155. %% still listed after stopped
  156. ReleaseNameBin = list_to_binary(ReleaseName),
  157. PluginVsnBin = list_to_binary(PluginVsn),
  158. ?assertMatch(
  159. [
  160. #{
  161. <<"name">> := ReleaseNameBin,
  162. <<"rel_vsn">> := PluginVsnBin
  163. }
  164. ],
  165. emqx_plugins:list()
  166. ),
  167. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  168. ?assertEqual([], emqx_plugins:list()),
  169. ok.
  170. %% help function to create a info file.
  171. %% The file is in JSON format when built
  172. %% but since we are using hocon:load to load it
  173. %% ad-hoc test files can be in hocon format
  174. write_info_file(Config, NameVsn, Content) ->
  175. WorkDir = proplists:get_value(data_dir, Config),
  176. InfoFile = filename:join([WorkDir, NameVsn, "release.json"]),
  177. ok = filelib:ensure_dir(InfoFile),
  178. ok = file:write_file(InfoFile, Content).
  179. t_position({init, Config}) ->
  180. #{package := Package} = get_demo_plugin_package(),
  181. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  182. [{name_vsn, NameVsn} | Config];
  183. t_position({'end', _Config}) ->
  184. ok;
  185. t_position(Config) ->
  186. NameVsn = proplists:get_value(name_vsn, Config),
  187. ok = emqx_plugins:ensure_installed(NameVsn),
  188. ok = emqx_plugins:ensure_enabled(NameVsn),
  189. FakeInfo =
  190. "name=position, rel_vsn=\"2\", rel_apps=[\"position-9\"],"
  191. "description=\"desc fake position app\"",
  192. PosApp2 = <<"position-2">>,
  193. ok = write_info_file(Config, PosApp2, FakeInfo),
  194. %% fake a disabled plugin in config
  195. ok = emqx_plugins:ensure_state(PosApp2, {before, NameVsn}, false),
  196. ListFun = fun() ->
  197. lists:map(
  198. fun(
  199. #{<<"name">> := Name, <<"rel_vsn">> := Vsn}
  200. ) ->
  201. <<Name/binary, "-", Vsn/binary>>
  202. end,
  203. emqx_plugins:list()
  204. )
  205. end,
  206. ?assertEqual([PosApp2, list_to_binary(NameVsn)], ListFun()),
  207. emqx_plugins:ensure_enabled(PosApp2, {behind, NameVsn}),
  208. ?assertEqual([list_to_binary(NameVsn), PosApp2], ListFun()),
  209. ok = emqx_plugins:ensure_stopped(),
  210. ok = emqx_plugins:ensure_disabled(NameVsn),
  211. ok = emqx_plugins:ensure_disabled(PosApp2),
  212. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  213. ok = emqx_plugins:ensure_uninstalled(PosApp2),
  214. ?assertEqual([], emqx_plugins:list()),
  215. ok.
  216. t_start_restart_and_stop({init, Config}) ->
  217. #{package := Package} = get_demo_plugin_package(),
  218. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  219. [{name_vsn, NameVsn} | Config];
  220. t_start_restart_and_stop({'end', _Config}) ->
  221. ok;
  222. t_start_restart_and_stop(Config) ->
  223. NameVsn = proplists:get_value(name_vsn, Config),
  224. ok = emqx_plugins:ensure_installed(NameVsn),
  225. ok = emqx_plugins:ensure_enabled(NameVsn),
  226. FakeInfo =
  227. "name=bar, rel_vsn=\"2\", rel_apps=[\"bar-9\"],"
  228. "description=\"desc bar\"",
  229. Bar2 = <<"bar-2">>,
  230. ok = write_info_file(Config, Bar2, FakeInfo),
  231. %% fake a disabled plugin in config
  232. ok = emqx_plugins:ensure_state(Bar2, front, false),
  233. assert_app_running(emqx_plugin_template, false),
  234. ok = emqx_plugins:ensure_started(),
  235. assert_app_running(emqx_plugin_template, true),
  236. %% fake enable bar-2
  237. ok = emqx_plugins:ensure_state(Bar2, rear, true),
  238. %% should cause an error
  239. ?assertError(
  240. #{function := _, errors := [_ | _]},
  241. emqx_plugins:ensure_started()
  242. ),
  243. %% but demo plugin should still be running
  244. assert_app_running(emqx_plugin_template, true),
  245. %% stop all
  246. ok = emqx_plugins:ensure_stopped(),
  247. assert_app_running(emqx_plugin_template, false),
  248. ok = emqx_plugins:ensure_state(Bar2, rear, false),
  249. ok = emqx_plugins:restart(NameVsn),
  250. assert_app_running(emqx_plugin_template, true),
  251. %% repeat
  252. ok = emqx_plugins:restart(NameVsn),
  253. assert_app_running(emqx_plugin_template, true),
  254. ok = emqx_plugins:ensure_stopped(),
  255. ok = emqx_plugins:ensure_disabled(NameVsn),
  256. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  257. ok = emqx_plugins:ensure_uninstalled(Bar2),
  258. ?assertEqual([], emqx_plugins:list()),
  259. ok.
  260. t_enable_disable({init, Config}) ->
  261. #{package := Package} = get_demo_plugin_package(),
  262. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  263. [{name_vsn, NameVsn} | Config];
  264. t_enable_disable({'end', Config}) ->
  265. ok = emqx_plugins:ensure_uninstalled(proplists:get_value(name_vsn, Config));
  266. t_enable_disable(Config) ->
  267. NameVsn = proplists:get_value(name_vsn, Config),
  268. ok = emqx_plugins:ensure_installed(NameVsn),
  269. ?assertEqual([], emqx_plugins:configured()),
  270. ok = emqx_plugins:ensure_enabled(NameVsn),
  271. ?assertEqual([#{name_vsn => NameVsn, enable => true}], emqx_plugins:configured()),
  272. ok = emqx_plugins:ensure_disabled(NameVsn),
  273. ?assertEqual([#{name_vsn => NameVsn, enable => false}], emqx_plugins:configured()),
  274. ok = emqx_plugins:ensure_enabled(bin(NameVsn)),
  275. ?assertEqual([#{name_vsn => NameVsn, enable => true}], emqx_plugins:configured()),
  276. ?assertMatch(
  277. {error, #{
  278. reason := "bad_plugin_config_status",
  279. hint := "disable_the_plugin_first"
  280. }},
  281. emqx_plugins:ensure_uninstalled(NameVsn)
  282. ),
  283. ok = emqx_plugins:ensure_disabled(bin(NameVsn)),
  284. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  285. ?assertMatch({error, _}, emqx_plugins:ensure_enabled(NameVsn)),
  286. ?assertMatch({error, _}, emqx_plugins:ensure_disabled(NameVsn)),
  287. ok.
  288. assert_app_running(Name, true) ->
  289. AllApps = application:which_applications(),
  290. ?assertMatch({Name, _, _}, lists:keyfind(Name, 1, AllApps));
  291. assert_app_running(Name, false) ->
  292. AllApps = application:which_applications(),
  293. ?assertEqual(false, lists:keyfind(Name, 1, AllApps)).
  294. t_bad_tar_gz({init, Config}) ->
  295. Config;
  296. t_bad_tar_gz({'end', _Config}) ->
  297. ok;
  298. t_bad_tar_gz(Config) ->
  299. WorkDir = proplists:get_value(data_dir, Config),
  300. FakeTarTz = filename:join([WorkDir, "fake-vsn.tar.gz"]),
  301. ok = file:write_file(FakeTarTz, "a\n"),
  302. ?assertMatch(
  303. {error, #{
  304. reason := "bad_plugin_package",
  305. return := eof
  306. }},
  307. emqx_plugins:ensure_installed("fake-vsn")
  308. ),
  309. ?assertMatch(
  310. {error, #{
  311. reason := "failed_to_extract_plugin_package",
  312. return := not_found
  313. }},
  314. emqx_plugins:ensure_installed("nonexisting")
  315. ),
  316. ?assertEqual([], emqx_plugins:list()),
  317. ok = emqx_plugins:delete_package("fake-vsn"),
  318. %% idempotent
  319. ok = emqx_plugins:delete_package("fake-vsn").
  320. %% create with incomplete info file
  321. %% failed install attempts should not leave behind extracted dir
  322. t_bad_tar_gz2({init, Config}) ->
  323. WorkDir = proplists:get_value(data_dir, Config),
  324. NameVsn = "foo-0.2",
  325. %% this an invalid info file content (description missing)
  326. BadInfo = "name=foo, rel_vsn=\"0.2\", rel_apps=[foo]",
  327. ok = write_info_file(Config, NameVsn, BadInfo),
  328. TarGz = filename:join([WorkDir, NameVsn ++ ".tar.gz"]),
  329. ok = make_tar(WorkDir, NameVsn),
  330. [{tar_gz, TarGz}, {name_vsn, NameVsn} | Config];
  331. t_bad_tar_gz2({'end', Config}) ->
  332. NameVsn = ?config(name_vsn, Config),
  333. ok = emqx_plugins:delete_package(NameVsn),
  334. ok;
  335. t_bad_tar_gz2(Config) ->
  336. TarGz = ?config(tar_gz, Config),
  337. NameVsn = ?config(name_vsn, Config),
  338. ?assert(filelib:is_regular(TarGz)),
  339. %% failed to install, it also cleans up the bad content of .tar.gz file
  340. ?assertMatch({error, _}, emqx_plugins:ensure_installed(NameVsn)),
  341. ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))),
  342. %% but the tar.gz file is still around
  343. ?assert(filelib:is_regular(TarGz)),
  344. ok.
  345. %% test that we even cleanup content that doesn't match the expected name-vsn
  346. %% pattern
  347. t_tar_vsn_content_mismatch({init, Config}) ->
  348. WorkDir = proplists:get_value(data_dir, Config),
  349. NameVsn = "bad_tar-0.2",
  350. %% this an invalid info file content
  351. BadInfo = "name=foo, rel_vsn=\"0.2\", rel_apps=[\"foo-0.2\"], description=\"lorem ipsum\"",
  352. ok = write_info_file(Config, "foo-0.2", BadInfo),
  353. TarGz = filename:join([WorkDir, "bad_tar-0.2.tar.gz"]),
  354. ok = make_tar(WorkDir, "foo-0.2", NameVsn),
  355. file:delete(filename:join([WorkDir, "foo-0.2", "release.json"])),
  356. [{tar_gz, TarGz}, {name_vsn, NameVsn} | Config];
  357. t_tar_vsn_content_mismatch({'end', Config}) ->
  358. NameVsn = ?config(name_vsn, Config),
  359. ok = emqx_plugins:delete_package(NameVsn),
  360. ok;
  361. t_tar_vsn_content_mismatch(Config) ->
  362. TarGz = ?config(tar_gz, Config),
  363. NameVsn = ?config(name_vsn, Config),
  364. ?assert(filelib:is_regular(TarGz)),
  365. %% failed to install, it also cleans up content of the bad .tar.gz file even
  366. %% if in other directory
  367. ?assertMatch({error, _}, emqx_plugins:ensure_installed(NameVsn)),
  368. ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))),
  369. ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir("foo-0.2"))),
  370. %% the tar.gz file is still around
  371. ?assert(filelib:is_regular(TarGz)),
  372. ok.
  373. t_bad_info_json({init, Config}) ->
  374. Config;
  375. t_bad_info_json({'end', _}) ->
  376. ok;
  377. t_bad_info_json(Config) ->
  378. NameVsn = "test-2",
  379. ok = write_info_file(Config, NameVsn, "bad-syntax"),
  380. ?assertMatch(
  381. {error, #{
  382. error := "bad_info_file",
  383. return := {parse_error, _}
  384. }},
  385. emqx_plugins:describe(NameVsn)
  386. ),
  387. ok = write_info_file(Config, NameVsn, "{\"bad\": \"obj\"}"),
  388. ?assertMatch(
  389. {error, #{
  390. error := "bad_info_file_content",
  391. mandatory_fields := _
  392. }},
  393. emqx_plugins:describe(NameVsn)
  394. ),
  395. ?assertEqual([], emqx_plugins:list()),
  396. emqx_plugins:purge(NameVsn),
  397. ok.
  398. t_elixir_plugin({init, Config}) ->
  399. Opts0 =
  400. #{
  401. release_name => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_RELEASE_NAME,
  402. git_url => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_URL,
  403. vsn => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN,
  404. tag => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_TAG,
  405. shdir => emqx_plugins:install_dir()
  406. },
  407. Opts = #{package := Package} = get_demo_plugin_package(Opts0),
  408. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  409. [
  410. {name_vsn, NameVsn},
  411. {plugin_opts, Opts}
  412. | Config
  413. ];
  414. t_elixir_plugin({'end', _Config}) ->
  415. ok;
  416. t_elixir_plugin(Config) ->
  417. NameVsn = proplists:get_value(name_vsn, Config),
  418. #{
  419. release_name := ReleaseName,
  420. vsn := PluginVsn
  421. } = proplists:get_value(plugin_opts, Config),
  422. ok = emqx_plugins:ensure_installed(NameVsn),
  423. %% idempotent
  424. ok = emqx_plugins:ensure_installed(NameVsn),
  425. {ok, Info} = emqx_plugins:read_plugin(NameVsn, #{}),
  426. ?assertEqual([Info], emqx_plugins:list()),
  427. %% start
  428. ok = emqx_plugins:ensure_started(NameVsn),
  429. ok = assert_app_running(elixir_plugin_template, true),
  430. ok = assert_app_running(hallux, true),
  431. %% start (idempotent)
  432. ok = emqx_plugins:ensure_started(bin(NameVsn)),
  433. ok = assert_app_running(elixir_plugin_template, true),
  434. ok = assert_app_running(hallux, true),
  435. %% call an elixir function
  436. 1 = 'Elixir.ElixirPluginTemplate':ping(),
  437. 3 = 'Elixir.Kernel':'+'(1, 2),
  438. %% running app can not be un-installed
  439. ?assertMatch(
  440. {error, _},
  441. emqx_plugins:ensure_uninstalled(NameVsn)
  442. ),
  443. %% stop
  444. ok = emqx_plugins:ensure_stopped(NameVsn),
  445. ok = assert_app_running(elixir_plugin_template, false),
  446. ok = assert_app_running(hallux, false),
  447. %% stop (idempotent)
  448. ok = emqx_plugins:ensure_stopped(bin(NameVsn)),
  449. ok = assert_app_running(elixir_plugin_template, false),
  450. ok = assert_app_running(hallux, false),
  451. %% still listed after stopped
  452. ReleaseNameBin = list_to_binary(ReleaseName),
  453. PluginVsnBin = list_to_binary(PluginVsn),
  454. ?assertMatch(
  455. [
  456. #{
  457. <<"name">> := ReleaseNameBin,
  458. <<"rel_vsn">> := PluginVsnBin
  459. }
  460. ],
  461. emqx_plugins:list()
  462. ),
  463. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  464. ?assertEqual([], emqx_plugins:list()),
  465. ok.
  466. group_t_copy_plugin_to_a_new_node({init, Config}) ->
  467. WorkDir = proplists:get_value(data_dir, Config),
  468. FromInstallDir = filename:join(WorkDir, atom_to_list(plugins_copy_from)),
  469. file:del_dir_r(FromInstallDir),
  470. ok = filelib:ensure_path(FromInstallDir),
  471. ToInstallDir = filename:join(WorkDir, atom_to_list(plugins_copy_to)),
  472. file:del_dir_r(ToInstallDir),
  473. ok = filelib:ensure_path(ToInstallDir),
  474. #{package := Package, release_name := PluginName} = get_demo_plugin_package(FromInstallDir),
  475. [{CopyFrom, CopyFromOpts}, {CopyTo, CopyToOpts}] =
  476. emqx_common_test_helpers:emqx_cluster(
  477. [
  478. {core, plugins_copy_from},
  479. {core, plugins_copy_to}
  480. ],
  481. #{
  482. apps => [emqx_conf, emqx_plugins],
  483. env => [
  484. {emqx, init_config_load_done, false},
  485. {emqx, boot_modules, []}
  486. ],
  487. load_schema => false
  488. }
  489. ),
  490. CopyFromNode = emqx_common_test_helpers:start_slave(
  491. CopyFrom, maps:remove(join_to, CopyFromOpts)
  492. ),
  493. ok = rpc:call(CopyFromNode, emqx_plugins, put_config, [install_dir, FromInstallDir]),
  494. CopyToNode = emqx_common_test_helpers:start_slave(CopyTo, maps:remove(join_to, CopyToOpts)),
  495. ok = rpc:call(CopyToNode, emqx_plugins, put_config, [install_dir, ToInstallDir]),
  496. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  497. ok = rpc:call(CopyFromNode, emqx_plugins, ensure_installed, [NameVsn]),
  498. ok = rpc:call(CopyFromNode, emqx_plugins, ensure_started, [NameVsn]),
  499. ok = rpc:call(CopyFromNode, emqx_plugins, ensure_enabled, [NameVsn]),
  500. case proplists:get_bool(remove_tar, Config) of
  501. true ->
  502. %% Test the case when a plugin is installed, but its original tar file is removed
  503. %% and must be re-created
  504. ok = file:delete(filename:join(FromInstallDir, NameVsn ++ ?PACKAGE_SUFFIX));
  505. false ->
  506. ok
  507. end,
  508. [
  509. {from_install_dir, FromInstallDir},
  510. {to_install_dir, ToInstallDir},
  511. {copy_from_node, CopyFromNode},
  512. {copy_to_node, CopyToNode},
  513. {name_vsn, NameVsn},
  514. {plugin_name, PluginName}
  515. | Config
  516. ];
  517. group_t_copy_plugin_to_a_new_node({'end', Config}) ->
  518. CopyFromNode = proplists:get_value(copy_from_node, Config),
  519. CopyToNode = proplists:get_value(copy_to_node, Config),
  520. ok = rpc:call(CopyFromNode, emqx_config, delete_override_conf_files, []),
  521. ok = rpc:call(CopyToNode, emqx_config, delete_override_conf_files, []),
  522. rpc:call(CopyToNode, ekka, leave, []),
  523. rpc:call(CopyFromNode, ekka, leave, []),
  524. ok = emqx_common_test_helpers:stop_slave(CopyToNode),
  525. ok = emqx_common_test_helpers:stop_slave(CopyFromNode),
  526. ok = file:del_dir_r(proplists:get_value(to_install_dir, Config)),
  527. ok = file:del_dir_r(proplists:get_value(from_install_dir, Config));
  528. group_t_copy_plugin_to_a_new_node(Config) ->
  529. CopyFromNode = proplists:get_value(copy_from_node, Config),
  530. CopyToNode = proplists:get_value(copy_to_node, Config),
  531. CopyToDir = proplists:get_value(to_install_dir, Config),
  532. CopyFromPluginsState = rpc:call(CopyFromNode, emqx_plugins, get_config, [[states], []]),
  533. NameVsn = proplists:get_value(name_vsn, Config),
  534. PluginName = proplists:get_value(plugin_name, Config),
  535. PluginApp = list_to_atom(PluginName),
  536. ?assertMatch([#{enable := true, name_vsn := NameVsn}], CopyFromPluginsState),
  537. ?assert(
  538. proplists:is_defined(
  539. PluginApp,
  540. rpc:call(CopyFromNode, application, which_applications, [])
  541. )
  542. ),
  543. ?assertEqual([], filelib:wildcard(filename:join(CopyToDir, "**"))),
  544. %% Check that a new node doesn't have this plugin before it joins the cluster
  545. ?assertEqual([], rpc:call(CopyToNode, emqx_conf, get, [[plugins, states], []])),
  546. ?assertMatch({error, _}, rpc:call(CopyToNode, emqx_plugins, describe, [NameVsn])),
  547. ?assertNot(
  548. proplists:is_defined(
  549. PluginApp,
  550. rpc:call(CopyToNode, application, which_applications, [])
  551. )
  552. ),
  553. ok = rpc:call(CopyToNode, ekka, join, [CopyFromNode]),
  554. %% Mimic cluster-override conf copying
  555. ok = rpc:call(CopyToNode, emqx_plugins, put_config, [[states], CopyFromPluginsState]),
  556. %% Plugin copying is triggered upon app restart on a new node.
  557. %% This is similar to emqx_conf, which copies cluster-override conf upon start,
  558. %% see: emqx_conf_app:init_conf/0
  559. ok = rpc:call(CopyToNode, application, stop, [emqx_plugins]),
  560. {ok, _} = rpc:call(CopyToNode, application, ensure_all_started, [emqx_plugins]),
  561. ?assertMatch(
  562. {ok, #{running_status := running, config_status := enabled}},
  563. rpc:call(CopyToNode, emqx_plugins, describe, [NameVsn])
  564. ).
  565. make_tar(Cwd, NameWithVsn) ->
  566. make_tar(Cwd, NameWithVsn, NameWithVsn).
  567. make_tar(Cwd, NameWithVsn, TarfileVsn) ->
  568. {ok, OriginalCwd} = file:get_cwd(),
  569. ok = file:set_cwd(Cwd),
  570. try
  571. Files = filelib:wildcard(NameWithVsn ++ "/**"),
  572. TarFile = TarfileVsn ++ ".tar.gz",
  573. ok = erl_tar:create(TarFile, Files, [compressed])
  574. after
  575. file:set_cwd(OriginalCwd)
  576. end.