emqx_plugins_SUITE.erl 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  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], [
  43. group_t_copy_plugin_to_a_new_node,
  44. group_t_copy_plugin_to_a_new_node_single_node,
  45. group_t_cluster_leave
  46. ]},
  47. {create_tar_copy_plugin, [sequence], [group_t_copy_plugin_to_a_new_node]}
  48. ].
  49. init_per_group(copy_plugin, Config) ->
  50. Config;
  51. init_per_group(create_tar_copy_plugin, Config) ->
  52. [{remove_tar, true} | Config].
  53. end_per_group(_Group, _Config) ->
  54. ok.
  55. init_per_suite(Config) ->
  56. WorkDir = proplists:get_value(data_dir, Config),
  57. filelib:ensure_path(WorkDir),
  58. OrigInstallDir = emqx_plugins:get_config(install_dir, undefined),
  59. emqx_common_test_helpers:start_apps([emqx_conf, emqx_plugins]),
  60. emqx_plugins:put_config(install_dir, WorkDir),
  61. [{orig_install_dir, OrigInstallDir} | Config].
  62. end_per_suite(Config) ->
  63. emqx_common_test_helpers:boot_modules(all),
  64. emqx_config:erase(plugins),
  65. %% restore config
  66. case proplists:get_value(orig_install_dir, Config) of
  67. undefined -> ok;
  68. OrigInstallDir -> emqx_plugins:put_config(install_dir, OrigInstallDir)
  69. end,
  70. emqx_common_test_helpers:stop_apps([emqx_plugins, emqx_conf]),
  71. ok.
  72. init_per_testcase(TestCase, Config) ->
  73. emqx_plugins:put_configured([]),
  74. lists:foreach(
  75. fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) ->
  76. emqx_plugins:purge(bin([Name, "-", Vsn]))
  77. end,
  78. emqx_plugins:list()
  79. ),
  80. ?MODULE:TestCase({init, Config}).
  81. end_per_testcase(TestCase, Config) ->
  82. emqx_plugins:put_configured([]),
  83. ?MODULE:TestCase({'end', Config}).
  84. get_demo_plugin_package() ->
  85. get_demo_plugin_package(emqx_plugins:install_dir()).
  86. get_demo_plugin_package(
  87. #{
  88. release_name := ReleaseName,
  89. git_url := GitUrl,
  90. vsn := PluginVsn,
  91. tag := ReleaseTag,
  92. shdir := WorkDir
  93. } = Opts
  94. ) ->
  95. TargetName = lists:flatten([ReleaseName, "-", PluginVsn, ?PACKAGE_SUFFIX]),
  96. FileURI = lists:flatten(lists:join("/", [GitUrl, ReleaseTag, TargetName])),
  97. {ok, {_, _, PluginBin}} = httpc:request(FileURI),
  98. Pkg = filename:join([
  99. WorkDir,
  100. TargetName
  101. ]),
  102. ok = file:write_file(Pkg, PluginBin),
  103. Opts#{package => Pkg};
  104. get_demo_plugin_package(Dir) ->
  105. get_demo_plugin_package(
  106. #{
  107. release_name => ?EMQX_PLUGIN_TEMPLATE_RELEASE_NAME,
  108. git_url => ?EMQX_PLUGIN_TEMPLATE_URL,
  109. vsn => ?EMQX_PLUGIN_TEMPLATE_VSN,
  110. tag => ?EMQX_PLUGIN_TEMPLATE_TAG,
  111. shdir => Dir
  112. }
  113. ).
  114. bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
  115. bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8);
  116. bin(B) when is_binary(B) -> B.
  117. t_demo_install_start_stop_uninstall({init, Config}) ->
  118. Opts = #{package := Package} = get_demo_plugin_package(),
  119. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  120. [
  121. {name_vsn, NameVsn},
  122. {plugin_opts, Opts}
  123. | Config
  124. ];
  125. t_demo_install_start_stop_uninstall({'end', _Config}) ->
  126. ok;
  127. t_demo_install_start_stop_uninstall(Config) ->
  128. NameVsn = proplists:get_value(name_vsn, Config),
  129. #{
  130. release_name := ReleaseName,
  131. vsn := PluginVsn
  132. } = proplists:get_value(plugin_opts, Config),
  133. ok = emqx_plugins:ensure_installed(NameVsn),
  134. %% idempotent
  135. ok = emqx_plugins:ensure_installed(NameVsn),
  136. {ok, Info} = emqx_plugins:describe(NameVsn),
  137. ?assertEqual([maps:without([readme], Info)], emqx_plugins:list()),
  138. %% start
  139. ok = emqx_plugins:ensure_started(NameVsn),
  140. ok = assert_app_running(emqx_plugin_template, true),
  141. ok = assert_app_running(map_sets, true),
  142. %% start (idempotent)
  143. ok = emqx_plugins:ensure_started(bin(NameVsn)),
  144. ok = assert_app_running(emqx_plugin_template, true),
  145. ok = assert_app_running(map_sets, true),
  146. %% running app can not be un-installed
  147. ?assertMatch(
  148. {error, _},
  149. emqx_plugins:ensure_uninstalled(NameVsn)
  150. ),
  151. %% stop
  152. ok = emqx_plugins:ensure_stopped(NameVsn),
  153. ok = assert_app_running(emqx_plugin_template, false),
  154. ok = assert_app_running(map_sets, false),
  155. %% stop (idempotent)
  156. ok = emqx_plugins:ensure_stopped(bin(NameVsn)),
  157. ok = assert_app_running(emqx_plugin_template, false),
  158. ok = assert_app_running(map_sets, false),
  159. %% still listed after stopped
  160. ReleaseNameBin = list_to_binary(ReleaseName),
  161. PluginVsnBin = list_to_binary(PluginVsn),
  162. ?assertMatch(
  163. [
  164. #{
  165. <<"name">> := ReleaseNameBin,
  166. <<"rel_vsn">> := PluginVsnBin
  167. }
  168. ],
  169. emqx_plugins:list()
  170. ),
  171. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  172. ?assertEqual([], emqx_plugins:list()),
  173. ok.
  174. %% help function to create a info file.
  175. %% The file is in JSON format when built
  176. %% but since we are using hocon:load to load it
  177. %% ad-hoc test files can be in hocon format
  178. write_info_file(Config, NameVsn, Content) ->
  179. WorkDir = proplists:get_value(data_dir, Config),
  180. InfoFile = filename:join([WorkDir, NameVsn, "release.json"]),
  181. ok = filelib:ensure_dir(InfoFile),
  182. ok = file:write_file(InfoFile, Content).
  183. t_position({init, Config}) ->
  184. #{package := Package} = get_demo_plugin_package(),
  185. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  186. [{name_vsn, NameVsn} | Config];
  187. t_position({'end', _Config}) ->
  188. ok;
  189. t_position(Config) ->
  190. NameVsn = proplists:get_value(name_vsn, Config),
  191. ok = emqx_plugins:ensure_installed(NameVsn),
  192. ok = emqx_plugins:ensure_enabled(NameVsn),
  193. FakeInfo =
  194. "name=position, rel_vsn=\"2\", rel_apps=[\"position-9\"],"
  195. "description=\"desc fake position app\"",
  196. PosApp2 = <<"position-2">>,
  197. ok = write_info_file(Config, PosApp2, FakeInfo),
  198. %% fake a disabled plugin in config
  199. ok = ensure_state(PosApp2, {before, NameVsn}, false),
  200. ListFun = fun() ->
  201. lists:map(
  202. fun(
  203. #{<<"name">> := Name, <<"rel_vsn">> := Vsn}
  204. ) ->
  205. <<Name/binary, "-", Vsn/binary>>
  206. end,
  207. emqx_plugins:list()
  208. )
  209. end,
  210. ?assertEqual([PosApp2, list_to_binary(NameVsn)], ListFun()),
  211. emqx_plugins:ensure_enabled(PosApp2, {behind, NameVsn}),
  212. ?assertEqual([list_to_binary(NameVsn), PosApp2], ListFun()),
  213. ok = emqx_plugins:ensure_stopped(),
  214. ok = emqx_plugins:ensure_disabled(NameVsn),
  215. ok = emqx_plugins:ensure_disabled(PosApp2),
  216. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  217. ok = emqx_plugins:ensure_uninstalled(PosApp2),
  218. ?assertEqual([], emqx_plugins:list()),
  219. ok.
  220. t_start_restart_and_stop({init, Config}) ->
  221. #{package := Package} = get_demo_plugin_package(),
  222. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  223. [{name_vsn, NameVsn} | Config];
  224. t_start_restart_and_stop({'end', _Config}) ->
  225. ok;
  226. t_start_restart_and_stop(Config) ->
  227. NameVsn = proplists:get_value(name_vsn, Config),
  228. ok = emqx_plugins:ensure_installed(NameVsn),
  229. ok = emqx_plugins:ensure_enabled(NameVsn),
  230. FakeInfo =
  231. "name=bar, rel_vsn=\"2\", rel_apps=[\"bar-9\"],"
  232. "description=\"desc bar\"",
  233. Bar2 = <<"bar-2">>,
  234. ok = write_info_file(Config, Bar2, FakeInfo),
  235. %% fake a disabled plugin in config
  236. ok = ensure_state(Bar2, front, false),
  237. assert_app_running(emqx_plugin_template, false),
  238. ok = emqx_plugins:ensure_started(),
  239. assert_app_running(emqx_plugin_template, true),
  240. %% fake enable bar-2
  241. ok = ensure_state(Bar2, rear, true),
  242. %% should cause an error
  243. ?assertError(
  244. #{function := _, errors := [_ | _]},
  245. emqx_plugins:ensure_started()
  246. ),
  247. %% but demo plugin should still be running
  248. assert_app_running(emqx_plugin_template, true),
  249. %% stop all
  250. ok = emqx_plugins:ensure_stopped(),
  251. assert_app_running(emqx_plugin_template, false),
  252. ok = ensure_state(Bar2, rear, false),
  253. ok = emqx_plugins:restart(NameVsn),
  254. assert_app_running(emqx_plugin_template, true),
  255. %% repeat
  256. ok = emqx_plugins:restart(NameVsn),
  257. assert_app_running(emqx_plugin_template, true),
  258. ok = emqx_plugins:ensure_stopped(),
  259. ok = emqx_plugins:ensure_disabled(NameVsn),
  260. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  261. ok = emqx_plugins:ensure_uninstalled(Bar2),
  262. ?assertEqual([], emqx_plugins:list()),
  263. ok.
  264. t_enable_disable({init, Config}) ->
  265. #{package := Package} = get_demo_plugin_package(),
  266. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  267. [{name_vsn, NameVsn} | Config];
  268. t_enable_disable({'end', Config}) ->
  269. ok = emqx_plugins:ensure_uninstalled(proplists:get_value(name_vsn, Config));
  270. t_enable_disable(Config) ->
  271. NameVsn = proplists:get_value(name_vsn, Config),
  272. ok = emqx_plugins:ensure_installed(NameVsn),
  273. ?assertEqual([], emqx_plugins:configured()),
  274. ok = emqx_plugins:ensure_enabled(NameVsn),
  275. ?assertEqual([#{name_vsn => NameVsn, enable => true}], emqx_plugins:configured()),
  276. ok = emqx_plugins:ensure_disabled(NameVsn),
  277. ?assertEqual([#{name_vsn => NameVsn, enable => false}], emqx_plugins:configured()),
  278. ok = emqx_plugins:ensure_enabled(bin(NameVsn)),
  279. ?assertEqual([#{name_vsn => NameVsn, enable => true}], emqx_plugins:configured()),
  280. ?assertMatch(
  281. {error, #{
  282. reason := "bad_plugin_config_status",
  283. hint := "disable_the_plugin_first"
  284. }},
  285. emqx_plugins:ensure_uninstalled(NameVsn)
  286. ),
  287. ok = emqx_plugins:ensure_disabled(bin(NameVsn)),
  288. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  289. ?assertMatch({error, _}, emqx_plugins:ensure_enabled(NameVsn)),
  290. ?assertMatch({error, _}, emqx_plugins:ensure_disabled(NameVsn)),
  291. ok.
  292. assert_app_running(Name, true) ->
  293. AllApps = application:which_applications(),
  294. ?assertMatch({Name, _, _}, lists:keyfind(Name, 1, AllApps));
  295. assert_app_running(Name, false) ->
  296. AllApps = application:which_applications(),
  297. ?assertEqual(false, lists:keyfind(Name, 1, AllApps)).
  298. t_bad_tar_gz({init, Config}) ->
  299. Config;
  300. t_bad_tar_gz({'end', _Config}) ->
  301. ok;
  302. t_bad_tar_gz(Config) ->
  303. WorkDir = proplists:get_value(data_dir, Config),
  304. FakeTarTz = filename:join([WorkDir, "fake-vsn.tar.gz"]),
  305. ok = file:write_file(FakeTarTz, "a\n"),
  306. ?assertMatch(
  307. {error, #{
  308. reason := "bad_plugin_package",
  309. return := eof
  310. }},
  311. emqx_plugins:ensure_installed("fake-vsn")
  312. ),
  313. ?assertMatch(
  314. {error, #{
  315. reason := "failed_to_extract_plugin_package",
  316. return := not_found
  317. }},
  318. emqx_plugins:ensure_installed("nonexisting")
  319. ),
  320. ?assertEqual([], emqx_plugins:list()),
  321. ok = emqx_plugins:delete_package("fake-vsn"),
  322. %% idempotent
  323. ok = emqx_plugins:delete_package("fake-vsn").
  324. %% create with incomplete info file
  325. %% failed install attempts should not leave behind extracted dir
  326. t_bad_tar_gz2({init, Config}) ->
  327. WorkDir = proplists:get_value(data_dir, Config),
  328. NameVsn = "foo-0.2",
  329. %% this an invalid info file content (description missing)
  330. BadInfo = "name=foo, rel_vsn=\"0.2\", rel_apps=[foo]",
  331. ok = write_info_file(Config, NameVsn, BadInfo),
  332. TarGz = filename:join([WorkDir, NameVsn ++ ".tar.gz"]),
  333. ok = make_tar(WorkDir, NameVsn),
  334. [{tar_gz, TarGz}, {name_vsn, NameVsn} | Config];
  335. t_bad_tar_gz2({'end', Config}) ->
  336. NameVsn = ?config(name_vsn, Config),
  337. ok = emqx_plugins:delete_package(NameVsn),
  338. ok;
  339. t_bad_tar_gz2(Config) ->
  340. TarGz = ?config(tar_gz, Config),
  341. NameVsn = ?config(name_vsn, Config),
  342. ?assert(filelib:is_regular(TarGz)),
  343. %% failed to install, it also cleans up the bad content of .tar.gz file
  344. ?assertMatch({error, _}, emqx_plugins:ensure_installed(NameVsn)),
  345. ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))),
  346. %% but the tar.gz file is still around
  347. ?assert(filelib:is_regular(TarGz)),
  348. ok.
  349. %% test that we even cleanup content that doesn't match the expected name-vsn
  350. %% pattern
  351. t_tar_vsn_content_mismatch({init, Config}) ->
  352. WorkDir = proplists:get_value(data_dir, Config),
  353. NameVsn = "bad_tar-0.2",
  354. %% this an invalid info file content
  355. BadInfo = "name=foo, rel_vsn=\"0.2\", rel_apps=[\"foo-0.2\"], description=\"lorem ipsum\"",
  356. ok = write_info_file(Config, "foo-0.2", BadInfo),
  357. TarGz = filename:join([WorkDir, "bad_tar-0.2.tar.gz"]),
  358. ok = make_tar(WorkDir, "foo-0.2", NameVsn),
  359. file:delete(filename:join([WorkDir, "foo-0.2", "release.json"])),
  360. [{tar_gz, TarGz}, {name_vsn, NameVsn} | Config];
  361. t_tar_vsn_content_mismatch({'end', Config}) ->
  362. NameVsn = ?config(name_vsn, Config),
  363. ok = emqx_plugins:delete_package(NameVsn),
  364. ok;
  365. t_tar_vsn_content_mismatch(Config) ->
  366. TarGz = ?config(tar_gz, Config),
  367. NameVsn = ?config(name_vsn, Config),
  368. ?assert(filelib:is_regular(TarGz)),
  369. %% failed to install, it also cleans up content of the bad .tar.gz file even
  370. %% if in other directory
  371. ?assertMatch({error, _}, emqx_plugins:ensure_installed(NameVsn)),
  372. ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))),
  373. ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir("foo-0.2"))),
  374. %% the tar.gz file is still around
  375. ?assert(filelib:is_regular(TarGz)),
  376. ok.
  377. t_bad_info_json({init, Config}) ->
  378. Config;
  379. t_bad_info_json({'end', _}) ->
  380. ok;
  381. t_bad_info_json(Config) ->
  382. NameVsn = "test-2",
  383. ok = write_info_file(Config, NameVsn, "bad-syntax"),
  384. ?assertMatch(
  385. {error, #{
  386. error := "bad_info_file",
  387. return := {parse_error, _}
  388. }},
  389. emqx_plugins:describe(NameVsn)
  390. ),
  391. ok = write_info_file(Config, NameVsn, "{\"bad\": \"obj\"}"),
  392. ?assertMatch(
  393. {error, #{
  394. error := "bad_info_file_content",
  395. mandatory_fields := _
  396. }},
  397. emqx_plugins:describe(NameVsn)
  398. ),
  399. ?assertEqual([], emqx_plugins:list()),
  400. emqx_plugins:purge(NameVsn),
  401. ok.
  402. t_elixir_plugin({init, Config}) ->
  403. Opts0 =
  404. #{
  405. release_name => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_RELEASE_NAME,
  406. git_url => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_URL,
  407. vsn => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN,
  408. tag => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_TAG,
  409. shdir => emqx_plugins:install_dir()
  410. },
  411. Opts = #{package := Package} = get_demo_plugin_package(Opts0),
  412. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  413. [
  414. {name_vsn, NameVsn},
  415. {plugin_opts, Opts}
  416. | Config
  417. ];
  418. t_elixir_plugin({'end', _Config}) ->
  419. ok;
  420. t_elixir_plugin(Config) ->
  421. NameVsn = proplists:get_value(name_vsn, Config),
  422. #{
  423. release_name := ReleaseName,
  424. vsn := PluginVsn
  425. } = proplists:get_value(plugin_opts, Config),
  426. ok = emqx_plugins:ensure_installed(NameVsn),
  427. %% idempotent
  428. ok = emqx_plugins:ensure_installed(NameVsn),
  429. {ok, Info} = emqx_plugins:read_plugin(NameVsn, #{}),
  430. ?assertEqual([Info], emqx_plugins:list()),
  431. %% start
  432. ok = emqx_plugins:ensure_started(NameVsn),
  433. ok = assert_app_running(elixir_plugin_template, true),
  434. ok = assert_app_running(hallux, true),
  435. %% start (idempotent)
  436. ok = emqx_plugins:ensure_started(bin(NameVsn)),
  437. ok = assert_app_running(elixir_plugin_template, true),
  438. ok = assert_app_running(hallux, true),
  439. %% call an elixir function
  440. 1 = 'Elixir.ElixirPluginTemplate':ping(),
  441. 3 = 'Elixir.Kernel':'+'(1, 2),
  442. %% running app can not be un-installed
  443. ?assertMatch(
  444. {error, _},
  445. emqx_plugins:ensure_uninstalled(NameVsn)
  446. ),
  447. %% stop
  448. ok = emqx_plugins:ensure_stopped(NameVsn),
  449. ok = assert_app_running(elixir_plugin_template, false),
  450. ok = assert_app_running(hallux, false),
  451. %% stop (idempotent)
  452. ok = emqx_plugins:ensure_stopped(bin(NameVsn)),
  453. ok = assert_app_running(elixir_plugin_template, false),
  454. ok = assert_app_running(hallux, false),
  455. %% still listed after stopped
  456. ReleaseNameBin = list_to_binary(ReleaseName),
  457. PluginVsnBin = list_to_binary(PluginVsn),
  458. ?assertMatch(
  459. [
  460. #{
  461. <<"name">> := ReleaseNameBin,
  462. <<"rel_vsn">> := PluginVsnBin
  463. }
  464. ],
  465. emqx_plugins:list()
  466. ),
  467. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  468. ?assertEqual([], emqx_plugins:list()),
  469. ok.
  470. t_load_config_from_cli({init, Config}) ->
  471. #{package := Package} = get_demo_plugin_package(),
  472. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  473. [{name_vsn, NameVsn} | Config];
  474. t_load_config_from_cli({'end', Config}) ->
  475. NameVsn = ?config(name_vsn, Config),
  476. ok = emqx_plugins:ensure_stopped(NameVsn),
  477. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  478. ok;
  479. t_load_config_from_cli(Config) when is_list(Config) ->
  480. NameVsn = ?config(name_vsn, Config),
  481. ok = emqx_plugins:ensure_installed(NameVsn),
  482. ?assertEqual([], emqx_plugins:configured()),
  483. ok = emqx_plugins:ensure_enabled(NameVsn),
  484. ok = emqx_plugins:ensure_started(NameVsn),
  485. Params0 = unused,
  486. ?assertMatch(
  487. {200, [#{running_status := [#{status := running}]}]},
  488. emqx_mgmt_api_plugins:list_plugins(get, Params0)
  489. ),
  490. %% Now we disable it via CLI loading
  491. Conf0 = emqx_config:get([plugins]),
  492. ?assertMatch(
  493. #{states := [#{enable := true}]},
  494. Conf0
  495. ),
  496. #{states := [Plugin0]} = Conf0,
  497. Conf1 = Conf0#{states := [Plugin0#{enable := false}]},
  498. Filename = filename:join(["/tmp", [?FUNCTION_NAME, ".hocon"]]),
  499. ok = file:write_file(Filename, hocon_pp:do(#{plugins => Conf1}, #{})),
  500. ok = emqx_conf_cli:conf(["load", Filename]),
  501. Conf2 = emqx_config:get([plugins]),
  502. ?assertMatch(
  503. #{states := [#{enable := false}]},
  504. Conf2
  505. ),
  506. ?assertMatch(
  507. {200, [#{running_status := [#{status := stopped}]}]},
  508. emqx_mgmt_api_plugins:list_plugins(get, Params0)
  509. ),
  510. %% Re-enable it via CLI loading
  511. ok = file:write_file(Filename, hocon_pp:do(#{plugins => Conf0}, #{})),
  512. ok = emqx_conf_cli:conf(["load", Filename]),
  513. Conf3 = emqx_config:get([plugins]),
  514. ?assertMatch(
  515. #{states := [#{enable := true}]},
  516. Conf3
  517. ),
  518. ?assertMatch(
  519. {200, [#{running_status := [#{status := running}]}]},
  520. emqx_mgmt_api_plugins:list_plugins(get, Params0)
  521. ),
  522. ok.
  523. group_t_copy_plugin_to_a_new_node({init, Config}) ->
  524. WorkDir = proplists:get_value(data_dir, Config),
  525. FromInstallDir = filename:join(WorkDir, atom_to_list(plugins_copy_from)),
  526. file:del_dir_r(FromInstallDir),
  527. ok = filelib:ensure_path(FromInstallDir),
  528. ToInstallDir = filename:join(WorkDir, atom_to_list(plugins_copy_to)),
  529. file:del_dir_r(ToInstallDir),
  530. ok = filelib:ensure_path(ToInstallDir),
  531. #{package := Package, release_name := PluginName} = get_demo_plugin_package(FromInstallDir),
  532. [{CopyFrom, CopyFromOpts}, {CopyTo, CopyToOpts}] =
  533. emqx_common_test_helpers:emqx_cluster(
  534. [
  535. {core, plugins_copy_from},
  536. {core, plugins_copy_to}
  537. ],
  538. #{
  539. apps => [emqx_conf, emqx_plugins],
  540. env => [
  541. {emqx, boot_modules, []}
  542. ],
  543. load_schema => false
  544. }
  545. ),
  546. CopyFromNode = emqx_common_test_helpers:start_slave(
  547. CopyFrom, maps:remove(join_to, CopyFromOpts)
  548. ),
  549. ok = rpc:call(CopyFromNode, emqx_plugins, put_config, [install_dir, FromInstallDir]),
  550. CopyToNode = emqx_common_test_helpers:start_slave(CopyTo, maps:remove(join_to, CopyToOpts)),
  551. ok = rpc:call(CopyToNode, emqx_plugins, put_config, [install_dir, ToInstallDir]),
  552. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  553. ok = rpc:call(CopyFromNode, emqx_plugins, ensure_installed, [NameVsn]),
  554. ok = rpc:call(CopyFromNode, emqx_plugins, ensure_started, [NameVsn]),
  555. ok = rpc:call(CopyFromNode, emqx_plugins, ensure_enabled, [NameVsn]),
  556. case proplists:get_bool(remove_tar, Config) of
  557. true ->
  558. %% Test the case when a plugin is installed, but its original tar file is removed
  559. %% and must be re-created
  560. ok = file:delete(filename:join(FromInstallDir, NameVsn ++ ?PACKAGE_SUFFIX));
  561. false ->
  562. ok
  563. end,
  564. [
  565. {from_install_dir, FromInstallDir},
  566. {to_install_dir, ToInstallDir},
  567. {copy_from_node, CopyFromNode},
  568. {copy_to_node, CopyToNode},
  569. {name_vsn, NameVsn},
  570. {plugin_name, PluginName}
  571. | Config
  572. ];
  573. group_t_copy_plugin_to_a_new_node({'end', Config}) ->
  574. CopyFromNode = proplists:get_value(copy_from_node, Config),
  575. CopyToNode = proplists:get_value(copy_to_node, Config),
  576. ok = rpc:call(CopyFromNode, emqx_config, delete_override_conf_files, []),
  577. ok = rpc:call(CopyToNode, emqx_config, delete_override_conf_files, []),
  578. rpc:call(CopyToNode, ekka, leave, []),
  579. rpc:call(CopyFromNode, ekka, leave, []),
  580. ok = emqx_common_test_helpers:stop_slave(CopyToNode),
  581. ok = emqx_common_test_helpers:stop_slave(CopyFromNode),
  582. ok = file:del_dir_r(proplists:get_value(to_install_dir, Config)),
  583. ok = file:del_dir_r(proplists:get_value(from_install_dir, Config));
  584. group_t_copy_plugin_to_a_new_node(Config) ->
  585. CopyFromNode = proplists:get_value(copy_from_node, Config),
  586. CopyToNode = proplists:get_value(copy_to_node, Config),
  587. CopyToDir = proplists:get_value(to_install_dir, Config),
  588. CopyFromPluginsState = rpc:call(CopyFromNode, emqx_plugins, get_config, [[states], []]),
  589. NameVsn = proplists:get_value(name_vsn, Config),
  590. PluginName = proplists:get_value(plugin_name, Config),
  591. PluginApp = list_to_atom(PluginName),
  592. ?assertMatch([#{enable := true, name_vsn := NameVsn}], CopyFromPluginsState),
  593. ?assert(
  594. proplists:is_defined(
  595. PluginApp,
  596. rpc:call(CopyFromNode, application, which_applications, [])
  597. )
  598. ),
  599. ?assertEqual([], filelib:wildcard(filename:join(CopyToDir, "**"))),
  600. %% Check that a new node doesn't have this plugin before it joins the cluster
  601. ?assertEqual([], rpc:call(CopyToNode, emqx_conf, get, [[plugins, states], []])),
  602. ?assertMatch({error, _}, rpc:call(CopyToNode, emqx_plugins, describe, [NameVsn])),
  603. ?assertNot(
  604. proplists:is_defined(
  605. PluginApp,
  606. rpc:call(CopyToNode, application, which_applications, [])
  607. )
  608. ),
  609. ok = rpc:call(CopyToNode, ekka, join, [CopyFromNode]),
  610. %% Mimic cluster-override conf copying
  611. ok = rpc:call(CopyToNode, emqx_plugins, put_config, [[states], CopyFromPluginsState]),
  612. %% Plugin copying is triggered upon app restart on a new node.
  613. %% This is similar to emqx_conf, which copies cluster-override conf upon start,
  614. %% see: emqx_conf_app:init_conf/0
  615. ok = rpc:call(CopyToNode, application, stop, [emqx_plugins]),
  616. {ok, _} = rpc:call(CopyToNode, application, ensure_all_started, [emqx_plugins]),
  617. ?assertMatch(
  618. {ok, #{running_status := running, config_status := enabled}},
  619. rpc:call(CopyToNode, emqx_plugins, describe, [NameVsn])
  620. ).
  621. %% checks that we can start a cluster with a lone node.
  622. group_t_copy_plugin_to_a_new_node_single_node({init, Config}) ->
  623. PrivDataDir = ?config(priv_dir, Config),
  624. ToInstallDir = filename:join(PrivDataDir, "plugins_copy_to"),
  625. file:del_dir_r(ToInstallDir),
  626. ok = filelib:ensure_path(ToInstallDir),
  627. #{package := Package, release_name := PluginName} = get_demo_plugin_package(ToInstallDir),
  628. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  629. [{CopyTo, CopyToOpts}] =
  630. emqx_common_test_helpers:emqx_cluster(
  631. [
  632. {core, plugins_copy_to}
  633. ],
  634. #{
  635. apps => [emqx_conf, emqx_plugins],
  636. env => [
  637. {emqx, boot_modules, []}
  638. ],
  639. env_handler => fun
  640. (emqx_plugins) ->
  641. ok = emqx_plugins:put_config(install_dir, ToInstallDir),
  642. %% this is to simulate an user setting the state
  643. %% via environment variables before starting the node
  644. ok = emqx_plugins:put_config(
  645. states,
  646. [#{name_vsn => NameVsn, enable => true}]
  647. ),
  648. ok;
  649. (_) ->
  650. ok
  651. end,
  652. priv_data_dir => PrivDataDir,
  653. schema_mod => emqx_conf_schema,
  654. peer_mod => slave,
  655. load_schema => true
  656. }
  657. ),
  658. [
  659. {to_install_dir, ToInstallDir},
  660. {copy_to_node_name, CopyTo},
  661. {copy_to_opts, CopyToOpts},
  662. {name_vsn, NameVsn},
  663. {plugin_name, PluginName}
  664. | Config
  665. ];
  666. group_t_copy_plugin_to_a_new_node_single_node({'end', Config}) ->
  667. CopyToNode = proplists:get_value(copy_to_node, Config),
  668. ok = emqx_common_test_helpers:stop_slave(CopyToNode),
  669. ok = file:del_dir_r(proplists:get_value(to_install_dir, Config)),
  670. ok;
  671. group_t_copy_plugin_to_a_new_node_single_node(Config) ->
  672. CopyTo = ?config(copy_to_node_name, Config),
  673. CopyToOpts = ?config(copy_to_opts, Config),
  674. ToInstallDir = ?config(to_install_dir, Config),
  675. NameVsn = proplists:get_value(name_vsn, Config),
  676. %% Start the node for the first time. The plugin should start
  677. %% successfully even if it's not extracted yet. Simply starting
  678. %% the node would crash if not working properly.
  679. CopyToNode = emqx_common_test_helpers:start_slave(CopyTo, CopyToOpts),
  680. ct:pal("~p config:\n ~p", [
  681. CopyToNode, erpc:call(CopyToNode, emqx_plugins, get_config, [[], #{}])
  682. ]),
  683. ct:pal("~p install_dir:\n ~p", [
  684. CopyToNode, erpc:call(CopyToNode, file, list_dir, [ToInstallDir])
  685. ]),
  686. ?assertMatch(
  687. {ok, #{running_status := running, config_status := enabled}},
  688. rpc:call(CopyToNode, emqx_plugins, describe, [NameVsn])
  689. ),
  690. ok.
  691. group_t_cluster_leave({init, Config}) ->
  692. PrivDataDir = ?config(priv_dir, Config),
  693. ToInstallDir = filename:join(PrivDataDir, "plugins_copy_to"),
  694. file:del_dir_r(ToInstallDir),
  695. ok = filelib:ensure_path(ToInstallDir),
  696. #{package := Package, release_name := PluginName} = get_demo_plugin_package(ToInstallDir),
  697. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  698. Cluster =
  699. emqx_common_test_helpers:emqx_cluster(
  700. [core, core],
  701. #{
  702. apps => [emqx_conf, emqx_plugins],
  703. env => [
  704. {emqx, boot_modules, []}
  705. ],
  706. env_handler => fun
  707. (emqx_plugins) ->
  708. ok = emqx_plugins:put_config(install_dir, ToInstallDir),
  709. %% this is to simulate an user setting the state
  710. %% via environment variables before starting the node
  711. ok = emqx_plugins:put_config(
  712. states,
  713. [#{name_vsn => NameVsn, enable => true}]
  714. ),
  715. ok;
  716. (_) ->
  717. ok
  718. end,
  719. priv_data_dir => PrivDataDir,
  720. schema_mod => emqx_conf_schema,
  721. peer_mod => slave,
  722. load_schema => true
  723. }
  724. ),
  725. Nodes = [emqx_common_test_helpers:start_slave(Name, Opts) || {Name, Opts} <- Cluster],
  726. [
  727. {to_install_dir, ToInstallDir},
  728. {cluster, Cluster},
  729. {nodes, Nodes},
  730. {name_vsn, NameVsn},
  731. {plugin_name, PluginName}
  732. | Config
  733. ];
  734. group_t_cluster_leave({'end', Config}) ->
  735. Nodes = proplists:get_value(nodes, Config),
  736. [ok = emqx_common_test_helpers:stop_slave(N) || N <- Nodes],
  737. ok = file:del_dir_r(proplists:get_value(to_install_dir, Config)),
  738. ok;
  739. group_t_cluster_leave(Config) ->
  740. [N1, N2] = ?config(nodes, Config),
  741. NameVsn = proplists:get_value(name_vsn, Config),
  742. ok = erpc:call(N1, emqx_plugins, ensure_installed, [NameVsn]),
  743. ok = erpc:call(N1, emqx_plugins, ensure_started, [NameVsn]),
  744. ok = erpc:call(N1, emqx_plugins, ensure_enabled, [NameVsn]),
  745. Params = unused,
  746. %% 2 nodes running
  747. ?assertMatch(
  748. {200, [#{running_status := [#{status := running}, #{status := running}]}]},
  749. erpc:call(N1, emqx_mgmt_api_plugins, list_plugins, [get, Params])
  750. ),
  751. ?assertMatch(
  752. {200, [#{running_status := [#{status := running}, #{status := running}]}]},
  753. erpc:call(N2, emqx_mgmt_api_plugins, list_plugins, [get, Params])
  754. ),
  755. %% Now, one node leaves the cluster.
  756. ok = erpc:call(N2, ekka, leave, []),
  757. %% Each node will no longer ask the plugin status to the other.
  758. ?assertMatch(
  759. {200, [#{running_status := [#{node := N1, status := running}]}]},
  760. erpc:call(N1, emqx_mgmt_api_plugins, list_plugins, [get, Params])
  761. ),
  762. ?assertMatch(
  763. {200, [#{running_status := [#{node := N2, status := running}]}]},
  764. erpc:call(N2, emqx_mgmt_api_plugins, list_plugins, [get, Params])
  765. ),
  766. ok.
  767. make_tar(Cwd, NameWithVsn) ->
  768. make_tar(Cwd, NameWithVsn, NameWithVsn).
  769. make_tar(Cwd, NameWithVsn, TarfileVsn) ->
  770. {ok, OriginalCwd} = file:get_cwd(),
  771. ok = file:set_cwd(Cwd),
  772. try
  773. Files = filelib:wildcard(NameWithVsn ++ "/**"),
  774. TarFile = TarfileVsn ++ ".tar.gz",
  775. ok = erl_tar:create(TarFile, Files, [compressed])
  776. after
  777. file:set_cwd(OriginalCwd)
  778. end.
  779. ensure_state(NameVsn, Position, Enabled) ->
  780. %% NOTE: this is an internal function that is (legacy) exported in test builds only...
  781. emqx_plugins:ensure_state(NameVsn, Position, Enabled, _ConfLocation = local).