emqx_plugins_SUITE.erl 16 KB

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