emqx_plugins_SUITE.erl 32 KB

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