emqx_plugins_SUITE.erl 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2019-2022 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("emqx/include/emqx.hrl").
  20. -include_lib("eunit/include/eunit.hrl").
  21. -define(EMQX_PLUGIN_TEMPLATE_VSN, "5.0.0-rc.3").
  22. -define(EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN, "0.1.0").
  23. -define(PACKAGE_SUFFIX, ".tar.gz").
  24. all() -> emqx_common_test_helpers:all(?MODULE).
  25. init_per_suite(Config) ->
  26. WorkDir = proplists:get_value(data_dir, Config),
  27. OrigInstallDir = emqx_plugins:get_config(install_dir, undefined),
  28. emqx_common_test_helpers:start_apps([emqx_conf]),
  29. emqx_plugins:put_config(install_dir, WorkDir),
  30. [{orig_install_dir, OrigInstallDir} | Config].
  31. end_per_suite(Config) ->
  32. emqx_common_test_helpers:boot_modules(all),
  33. emqx_config:erase(plugins),
  34. %% restore config
  35. case proplists:get_value(orig_install_dir, Config) of
  36. undefined -> ok;
  37. OrigInstallDir -> emqx_plugins:put_config(install_dir, OrigInstallDir)
  38. end,
  39. emqx_common_test_helpers:stop_apps([emqx_conf]),
  40. ok.
  41. init_per_testcase(TestCase, Config) ->
  42. emqx_plugins:put_configured([]),
  43. lists:foreach(
  44. fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) ->
  45. emqx_plugins:purge(bin([Name, "-", Vsn]))
  46. end,
  47. emqx_plugins:list()
  48. ),
  49. ?MODULE:TestCase({init, Config}).
  50. end_per_testcase(TestCase, Config) ->
  51. emqx_plugins:put_configured([]),
  52. ?MODULE:TestCase({'end', Config}).
  53. build_demo_plugin_package() ->
  54. build_demo_plugin_package(
  55. #{
  56. target_path => "_build/default/emqx_plugrel",
  57. release_name => "emqx_plugin_template",
  58. git_url => "https://github.com/emqx/emqx-plugin-template.git",
  59. vsn => ?EMQX_PLUGIN_TEMPLATE_VSN,
  60. workdir => "demo_src",
  61. shdir => emqx_plugins:install_dir()
  62. }
  63. ).
  64. build_demo_plugin_package(
  65. #{
  66. target_path := TargetPath,
  67. release_name := ReleaseName,
  68. git_url := GitUrl,
  69. vsn := PluginVsn,
  70. workdir := DemoWorkDir,
  71. shdir := WorkDir
  72. } = Opts
  73. ) ->
  74. BuildSh = filename:join([WorkDir, "build-demo-plugin.sh"]),
  75. Cmd = string:join(
  76. [
  77. BuildSh,
  78. PluginVsn,
  79. TargetPath,
  80. ReleaseName,
  81. GitUrl,
  82. DemoWorkDir
  83. ],
  84. " "
  85. ),
  86. case emqx_run_sh:do(Cmd, [{cd, WorkDir}]) of
  87. {ok, _} ->
  88. Pkg = filename:join([
  89. WorkDir,
  90. ReleaseName ++ "-" ++
  91. PluginVsn ++
  92. ?PACKAGE_SUFFIX
  93. ]),
  94. case filelib:is_regular(Pkg) of
  95. true -> Opts#{package => Pkg};
  96. false -> error(#{reason => unexpected_build_result, not_found => Pkg})
  97. end;
  98. {error, {Rc, Output}} ->
  99. io:format(user, "failed_to_build_demo_plugin, Exit = ~p, Output:~n~ts\n", [Rc, Output]),
  100. error(failed_to_build_demo_plugin)
  101. end.
  102. bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
  103. bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8);
  104. bin(B) when is_binary(B) -> B.
  105. t_demo_install_start_stop_uninstall({init, Config}) ->
  106. Opts = #{package := Package} = build_demo_plugin_package(),
  107. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  108. [
  109. {name_vsn, NameVsn},
  110. {plugin_opts, Opts}
  111. | Config
  112. ];
  113. t_demo_install_start_stop_uninstall({'end', _Config}) ->
  114. ok;
  115. t_demo_install_start_stop_uninstall(Config) ->
  116. NameVsn = proplists:get_value(name_vsn, Config),
  117. #{
  118. release_name := ReleaseName,
  119. vsn := PluginVsn
  120. } = proplists:get_value(plugin_opts, Config),
  121. ok = emqx_plugins:ensure_installed(NameVsn),
  122. %% idempotent
  123. ok = emqx_plugins:ensure_installed(NameVsn),
  124. {ok, Info} = emqx_plugins:describe(NameVsn),
  125. ?assertEqual([maps:without([readme], Info)], emqx_plugins:list()),
  126. %% start
  127. ok = emqx_plugins:ensure_started(NameVsn),
  128. ok = assert_app_running(emqx_plugin_template, true),
  129. ok = assert_app_running(map_sets, true),
  130. %% start (idempotent)
  131. ok = emqx_plugins:ensure_started(bin(NameVsn)),
  132. ok = assert_app_running(emqx_plugin_template, true),
  133. ok = assert_app_running(map_sets, true),
  134. %% running app can not be un-installed
  135. ?assertMatch(
  136. {error, _},
  137. emqx_plugins:ensure_uninstalled(NameVsn)
  138. ),
  139. %% stop
  140. ok = emqx_plugins:ensure_stopped(NameVsn),
  141. ok = assert_app_running(emqx_plugin_template, false),
  142. ok = assert_app_running(map_sets, false),
  143. %% stop (idempotent)
  144. ok = emqx_plugins:ensure_stopped(bin(NameVsn)),
  145. ok = assert_app_running(emqx_plugin_template, false),
  146. ok = assert_app_running(map_sets, false),
  147. %% still listed after stopped
  148. ReleaseNameBin = list_to_binary(ReleaseName),
  149. PluginVsnBin = list_to_binary(PluginVsn),
  150. ?assertMatch(
  151. [
  152. #{
  153. <<"name">> := ReleaseNameBin,
  154. <<"rel_vsn">> := PluginVsnBin
  155. }
  156. ],
  157. emqx_plugins:list()
  158. ),
  159. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  160. ?assertEqual([], emqx_plugins:list()),
  161. ok.
  162. %% help function to create a info file.
  163. %% The file is in JSON format when built
  164. %% but since we are using hocon:load to load it
  165. %% ad-hoc test files can be in hocon format
  166. write_info_file(Config, NameVsn, Content) ->
  167. WorkDir = proplists:get_value(data_dir, Config),
  168. InfoFile = filename:join([WorkDir, NameVsn, "release.json"]),
  169. ok = filelib:ensure_dir(InfoFile),
  170. ok = file:write_file(InfoFile, Content).
  171. t_position({init, Config}) ->
  172. #{package := Package} = build_demo_plugin_package(),
  173. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  174. [{name_vsn, NameVsn} | Config];
  175. t_position({'end', _Config}) ->
  176. ok;
  177. t_position(Config) ->
  178. NameVsn = proplists:get_value(name_vsn, Config),
  179. ok = emqx_plugins:ensure_installed(NameVsn),
  180. ok = emqx_plugins:ensure_enabled(NameVsn),
  181. FakeInfo =
  182. "name=position, rel_vsn=\"2\", rel_apps=[\"position-9\"],"
  183. "description=\"desc fake position app\"",
  184. PosApp2 = <<"position-2">>,
  185. ok = write_info_file(Config, PosApp2, FakeInfo),
  186. %% fake a disabled plugin in config
  187. ok = emqx_plugins:ensure_state(PosApp2, {before, NameVsn}, false),
  188. ListFun = fun() ->
  189. lists:map(
  190. fun(
  191. #{<<"name">> := Name, <<"rel_vsn">> := Vsn}
  192. ) ->
  193. <<Name/binary, "-", Vsn/binary>>
  194. end,
  195. emqx_plugins:list()
  196. )
  197. end,
  198. ?assertEqual([PosApp2, list_to_binary(NameVsn)], ListFun()),
  199. emqx_plugins:ensure_enabled(PosApp2, {behind, NameVsn}),
  200. ?assertEqual([list_to_binary(NameVsn), PosApp2], ListFun()),
  201. ok = emqx_plugins:ensure_stopped(),
  202. ok = emqx_plugins:ensure_disabled(NameVsn),
  203. ok = emqx_plugins:ensure_disabled(PosApp2),
  204. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  205. ok = emqx_plugins:ensure_uninstalled(PosApp2),
  206. ?assertEqual([], emqx_plugins:list()),
  207. ok.
  208. t_start_restart_and_stop({init, Config}) ->
  209. #{package := Package} = build_demo_plugin_package(),
  210. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  211. [{name_vsn, NameVsn} | Config];
  212. t_start_restart_and_stop({'end', _Config}) ->
  213. ok;
  214. t_start_restart_and_stop(Config) ->
  215. NameVsn = proplists:get_value(name_vsn, Config),
  216. ok = emqx_plugins:ensure_installed(NameVsn),
  217. ok = emqx_plugins:ensure_enabled(NameVsn),
  218. FakeInfo =
  219. "name=bar, rel_vsn=\"2\", rel_apps=[\"bar-9\"],"
  220. "description=\"desc bar\"",
  221. Bar2 = <<"bar-2">>,
  222. ok = write_info_file(Config, Bar2, FakeInfo),
  223. %% fake a disabled plugin in config
  224. ok = emqx_plugins:ensure_state(Bar2, front, false),
  225. assert_app_running(emqx_plugin_template, false),
  226. ok = emqx_plugins:ensure_started(),
  227. assert_app_running(emqx_plugin_template, true),
  228. %% fake enable bar-2
  229. ok = emqx_plugins:ensure_state(Bar2, rear, true),
  230. %% should cause an error
  231. ?assertError(
  232. #{function := _, errors := [_ | _]},
  233. emqx_plugins:ensure_started()
  234. ),
  235. %% but demo plugin should still be running
  236. assert_app_running(emqx_plugin_template, true),
  237. %% stop all
  238. ok = emqx_plugins:ensure_stopped(),
  239. assert_app_running(emqx_plugin_template, false),
  240. ok = emqx_plugins:ensure_state(Bar2, rear, false),
  241. ok = emqx_plugins:restart(NameVsn),
  242. assert_app_running(emqx_plugin_template, true),
  243. %% repeat
  244. ok = emqx_plugins:restart(NameVsn),
  245. assert_app_running(emqx_plugin_template, true),
  246. ok = emqx_plugins:ensure_stopped(),
  247. ok = emqx_plugins:ensure_disabled(NameVsn),
  248. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  249. ok = emqx_plugins:ensure_uninstalled(Bar2),
  250. ?assertEqual([], emqx_plugins:list()),
  251. ok.
  252. t_enable_disable({init, Config}) ->
  253. #{package := Package} = build_demo_plugin_package(),
  254. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  255. [{name_vsn, NameVsn} | Config];
  256. t_enable_disable({'end', Config}) ->
  257. ok = emqx_plugins:ensure_uninstalled(proplists:get_value(name_vsn, Config));
  258. t_enable_disable(Config) ->
  259. NameVsn = proplists:get_value(name_vsn, Config),
  260. ok = emqx_plugins:ensure_installed(NameVsn),
  261. ?assertEqual([], emqx_plugins:configured()),
  262. ok = emqx_plugins:ensure_enabled(NameVsn),
  263. ?assertEqual([#{name_vsn => NameVsn, enable => true}], emqx_plugins:configured()),
  264. ok = emqx_plugins:ensure_disabled(NameVsn),
  265. ?assertEqual([#{name_vsn => NameVsn, enable => false}], emqx_plugins:configured()),
  266. ok = emqx_plugins:ensure_enabled(bin(NameVsn)),
  267. ?assertEqual([#{name_vsn => NameVsn, enable => true}], emqx_plugins:configured()),
  268. ?assertMatch(
  269. {error, #{
  270. reason := "bad_plugin_config_status",
  271. hint := "disable_the_plugin_first"
  272. }},
  273. emqx_plugins:ensure_uninstalled(NameVsn)
  274. ),
  275. ok = emqx_plugins:ensure_disabled(bin(NameVsn)),
  276. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  277. ?assertMatch({error, _}, emqx_plugins:ensure_enabled(NameVsn)),
  278. ?assertMatch({error, _}, emqx_plugins:ensure_disabled(NameVsn)),
  279. ok.
  280. assert_app_running(Name, true) ->
  281. AllApps = application:which_applications(),
  282. ?assertMatch({Name, _, _}, lists:keyfind(Name, 1, AllApps));
  283. assert_app_running(Name, false) ->
  284. AllApps = application:which_applications(),
  285. ?assertEqual(false, lists:keyfind(Name, 1, AllApps)).
  286. t_bad_tar_gz({init, Config}) ->
  287. Config;
  288. t_bad_tar_gz({'end', _Config}) ->
  289. ok;
  290. t_bad_tar_gz(Config) ->
  291. WorkDir = proplists:get_value(data_dir, Config),
  292. FakeTarTz = filename:join([WorkDir, "fake-vsn.tar.gz"]),
  293. ok = file:write_file(FakeTarTz, "a\n"),
  294. ?assertMatch(
  295. {error, #{
  296. reason := "bad_plugin_package",
  297. return := eof
  298. }},
  299. emqx_plugins:ensure_installed("fake-vsn")
  300. ),
  301. ?assertMatch(
  302. {error, #{
  303. reason := "failed_to_extract_plugin_package",
  304. return := not_found
  305. }},
  306. emqx_plugins:ensure_installed("nonexisting")
  307. ),
  308. ?assertEqual([], emqx_plugins:list()),
  309. ok = emqx_plugins:delete_package("fake-vsn"),
  310. %% idempotent
  311. ok = emqx_plugins:delete_package("fake-vsn").
  312. %% create a corrupted .tar.gz
  313. %% failed install attempts should not leave behind extracted dir
  314. t_bad_tar_gz2({init, Config}) ->
  315. Config;
  316. t_bad_tar_gz2({'end', _Config}) ->
  317. ok;
  318. t_bad_tar_gz2(Config) ->
  319. WorkDir = proplists:get_value(data_dir, Config),
  320. NameVsn = "foo-0.2",
  321. %% this an invalid info file content
  322. BadInfo = "name=foo, rel_vsn=\"0.2\", rel_apps=[foo]",
  323. ok = write_info_file(Config, NameVsn, BadInfo),
  324. TarGz = filename:join([WorkDir, NameVsn ++ ".tar.gz"]),
  325. ok = make_tar(WorkDir, NameVsn),
  326. ?assert(filelib:is_regular(TarGz)),
  327. %% failed to install, it also cleans up the bad .tar.gz file
  328. ?assertMatch({error, _}, emqx_plugins:ensure_installed(NameVsn)),
  329. %% the tar.gz file is still around
  330. ?assert(filelib:is_regular(TarGz)),
  331. ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))),
  332. ok = emqx_plugins:delete_package(NameVsn).
  333. t_bad_info_json({init, Config}) ->
  334. Config;
  335. t_bad_info_json({'end', _}) ->
  336. ok;
  337. t_bad_info_json(Config) ->
  338. NameVsn = "test-2",
  339. ok = write_info_file(Config, NameVsn, "bad-syntax"),
  340. ?assertMatch(
  341. {error, #{
  342. error := "bad_info_file",
  343. return := {parse_error, _}
  344. }},
  345. emqx_plugins:describe(NameVsn)
  346. ),
  347. ok = write_info_file(Config, NameVsn, "{\"bad\": \"obj\"}"),
  348. ?assertMatch(
  349. {error, #{
  350. error := "bad_info_file_content",
  351. mandatory_fields := _
  352. }},
  353. emqx_plugins:describe(NameVsn)
  354. ),
  355. ?assertEqual([], emqx_plugins:list()),
  356. emqx_plugins:purge(NameVsn),
  357. ok.
  358. t_elixir_plugin({init, Config}) ->
  359. Opts0 =
  360. #{
  361. target_path => "_build/prod/plugrelex/elixir_plugin_template",
  362. release_name => "elixir_plugin_template",
  363. git_url => "https://github.com/emqx/emqx-elixir-plugin.git",
  364. vsn => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN,
  365. workdir => "demo_src_elixir",
  366. shdir => emqx_plugins:install_dir()
  367. },
  368. Opts = #{package := Package} = build_demo_plugin_package(Opts0),
  369. NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
  370. [
  371. {name_vsn, NameVsn},
  372. {plugin_opts, Opts}
  373. | Config
  374. ];
  375. t_elixir_plugin({'end', _Config}) ->
  376. ok;
  377. t_elixir_plugin(Config) ->
  378. NameVsn = proplists:get_value(name_vsn, Config),
  379. #{
  380. release_name := ReleaseName,
  381. vsn := PluginVsn
  382. } = proplists:get_value(plugin_opts, Config),
  383. ok = emqx_plugins:ensure_installed(NameVsn),
  384. %% idempotent
  385. ok = emqx_plugins:ensure_installed(NameVsn),
  386. {ok, Info} = emqx_plugins:read_plugin(NameVsn, #{}),
  387. ?assertEqual([Info], emqx_plugins:list()),
  388. %% start
  389. ok = emqx_plugins:ensure_started(NameVsn),
  390. ok = assert_app_running(elixir_plugin_template, true),
  391. ok = assert_app_running(hallux, true),
  392. %% start (idempotent)
  393. ok = emqx_plugins:ensure_started(bin(NameVsn)),
  394. ok = assert_app_running(elixir_plugin_template, true),
  395. ok = assert_app_running(hallux, true),
  396. %% call an elixir function
  397. 1 = 'Elixir.ElixirPluginTemplate':ping(),
  398. 3 = 'Elixir.Kernel':'+'(1, 2),
  399. %% running app can not be un-installed
  400. ?assertMatch(
  401. {error, _},
  402. emqx_plugins:ensure_uninstalled(NameVsn)
  403. ),
  404. %% stop
  405. ok = emqx_plugins:ensure_stopped(NameVsn),
  406. ok = assert_app_running(elixir_plugin_template, false),
  407. ok = assert_app_running(hallux, false),
  408. %% stop (idempotent)
  409. ok = emqx_plugins:ensure_stopped(bin(NameVsn)),
  410. ok = assert_app_running(elixir_plugin_template, false),
  411. ok = assert_app_running(hallux, false),
  412. %% still listed after stopped
  413. ReleaseNameBin = list_to_binary(ReleaseName),
  414. PluginVsnBin = list_to_binary(PluginVsn),
  415. ?assertMatch(
  416. [
  417. #{
  418. <<"name">> := ReleaseNameBin,
  419. <<"rel_vsn">> := PluginVsnBin
  420. }
  421. ],
  422. emqx_plugins:list()
  423. ),
  424. ok = emqx_plugins:ensure_uninstalled(NameVsn),
  425. ?assertEqual([], emqx_plugins:list()),
  426. ok.
  427. make_tar(Cwd, NameWithVsn) ->
  428. {ok, OriginalCwd} = file:get_cwd(),
  429. ok = file:set_cwd(Cwd),
  430. try
  431. Files = filelib:wildcard(NameWithVsn ++ "/**"),
  432. TarFile = NameWithVsn ++ ".tar.gz",
  433. ok = erl_tar:create(TarFile, Files, [compressed])
  434. after
  435. file:set_cwd(OriginalCwd)
  436. end.