emqx_plugins_SUITE.erl 14 KB

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