emqx_plugins_SUITE.erl 30 KB

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