From 14077ec43bc73e591b20475591db183a51a6701c Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 23 Nov 2023 17:31:59 +0100 Subject: [PATCH] feat(license): allow setting 'default' license key --- apps/emqx_license/src/emqx_license_parser.erl | 22 ++++++++++++++----- apps/emqx_license/src/emqx_license_schema.erl | 4 ++-- apps/emqx_license/test/emqx_license_SUITE.erl | 20 +++++------------ .../test/emqx_license_checker_SUITE.erl | 7 ++---- .../test/emqx_license_cli_SUITE.erl | 7 ++---- .../test/emqx_license_http_api_SUITE.erl | 20 ++++++++++++----- .../test/emqx_license_test_lib.erl | 10 +++++++++ rel/i18n/emqx_license_schema.hocon | 9 ++++---- 8 files changed, 58 insertions(+), 41 deletions(-) diff --git a/apps/emqx_license/src/emqx_license_parser.erl b/apps/emqx_license/src/emqx_license_parser.erl index 9a52d24fb..88304a6db 100644 --- a/apps/emqx_license/src/emqx_license_parser.erl +++ b/apps/emqx_license/src/emqx_license_parser.erl @@ -59,6 +59,12 @@ max_connections/1 ]). +%% for testing purpose +-export([ + default/0, + pubkey/0 +]). + %%-------------------------------------------------------------------- %% Behaviour %%-------------------------------------------------------------------- @@ -82,19 +88,18 @@ %% API %%-------------------------------------------------------------------- --ifdef(TEST). -pubkey() -> persistent_term:get(emqx_license_test_pubkey, ?PUBKEY). --else. pubkey() -> ?PUBKEY. --endif. +default() -> emqx_license_schema:default_license(). %% @doc Parse license key. %% If the license key is prefixed with "file://path/to/license/file", %% then the license key is read from the file. --spec parse(string() | binary()) -> {ok, license()} | {error, map()}. +-spec parse(default | string() | binary()) -> {ok, license()} | {error, map()}. parse(Content) -> - parse(iolist_to_binary(Content), pubkey()). + parse(to_bin(Content), ?MODULE:pubkey()). +parse(<<"default">>, PubKey) -> + parse(?MODULE:default(), PubKey); parse(<<"file://", Path/binary>> = FileKey, PubKey) -> case file:read_file(Path) of {ok, Content} -> @@ -159,3 +164,8 @@ do_parse(Content, Key, [Module | Modules], Errors) -> #{module => Module, error => Error, stacktrace => Stacktrace} | Errors ]) end. + +to_bin(A) when is_atom(A) -> + atom_to_binary(A); +to_bin(L) -> + iolist_to_binary(L). diff --git a/apps/emqx_license/src/emqx_license_schema.erl b/apps/emqx_license/src/emqx_license_schema.erl index f2b91811e..4d62f9be4 100644 --- a/apps/emqx_license/src/emqx_license_schema.erl +++ b/apps/emqx_license/src/emqx_license_schema.erl @@ -38,8 +38,8 @@ tags() -> fields(key_license) -> [ {key, #{ - type => binary(), - default => default_license(), + type => hoconsc:union([default, binary()]), + default => <<"default">>, %% so it's not logged sensitive => true, required => true, diff --git a/apps/emqx_license/test/emqx_license_SUITE.erl b/apps/emqx_license/test/emqx_license_SUITE.erl index 8a06fb540..4818ad9e6 100644 --- a/apps/emqx_license/test/emqx_license_SUITE.erl +++ b/apps/emqx_license/test/emqx_license_SUITE.erl @@ -16,12 +16,14 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> + emqx_license_test_lib:mock_parser(), _ = application:load(emqx_conf), emqx_config:save_schema_mod_and_names(emqx_license_schema), emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1), Config. end_per_suite(_) -> + emqx_license_test_lib:unmock_parser(), emqx_common_test_helpers:stop_apps([emqx_license]), ok. @@ -103,17 +105,7 @@ setup_test(TestCase, Config) when ), ok; (emqx_license) -> - LicensePath = filename:join(emqx_license:license_dir(), "emqx.lic"), - filelib:ensure_dir(LicensePath), - ok = file:write_file(LicensePath, LicenseKey), - LicConfig = #{type => file, file => LicensePath}, - emqx_config:put([license], LicConfig), - RawConfig = #{<<"type">> => file, <<"file">> => LicensePath}, - emqx_config:put_raw([<<"license">>], RawConfig), - ok = persistent_term:put( - emqx_license_test_pubkey, - emqx_license_test_lib:public_key_pem() - ), + set_special_configs(emqx_license), ok; (_) -> ok @@ -129,9 +121,9 @@ teardown_test(_TestCase, _Config) -> ok. set_special_configs(emqx_license) -> - Config = #{key => emqx_license_test_lib:default_license()}, + Config = #{key => default}, emqx_config:put([license], Config), - RawConfig = #{<<"key">> => emqx_license_test_lib:default_license()}, + RawConfig = #{<<"key">> => <<"default">>}, emqx_config:put_raw([<<"license">>], RawConfig); set_special_configs(_) -> ok. @@ -150,7 +142,7 @@ t_update_value(_Config) -> emqx_license:update_key("invalid.license") ), - LicenseValue = emqx_license_test_lib:default_license(), + LicenseValue = emqx_license_test_lib:default_test_license(), ?assertMatch( {ok, #{}}, diff --git a/apps/emqx_license/test/emqx_license_checker_SUITE.erl b/apps/emqx_license/test/emqx_license_checker_SUITE.erl index ce9945dd5..5733a09ce 100644 --- a/apps/emqx_license/test/emqx_license_checker_SUITE.erl +++ b/apps/emqx_license/test/emqx_license_checker_SUITE.erl @@ -16,15 +16,12 @@ all() -> init_per_suite(CtConfig) -> _ = application:load(emqx_conf), - ok = persistent_term:put( - emqx_license_test_pubkey, - emqx_license_test_lib:public_key_pem() - ), + emqx_license_test_lib:mock_parser(), ok = emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1), CtConfig. end_per_suite(_) -> - persistent_term:erase(emqx_license_test_pubkey), + emqx_license_test_lib:unmock_parser(), ok = emqx_common_test_helpers:stop_apps([emqx_license]). init_per_testcase(t_default_limits, Config) -> diff --git a/apps/emqx_license/test/emqx_license_cli_SUITE.erl b/apps/emqx_license/test/emqx_license_cli_SUITE.erl index 1c6282262..ed6593aac 100644 --- a/apps/emqx_license/test/emqx_license_cli_SUITE.erl +++ b/apps/emqx_license/test/emqx_license_cli_SUITE.erl @@ -24,15 +24,12 @@ end_per_suite(_) -> ok. init_per_testcase(_Case, Config) -> - ok = persistent_term:put( - emqx_license_test_pubkey, - emqx_license_test_lib:public_key_pem() - ), + emqx_license_test_lib:mock_parser(), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), Config. end_per_testcase(_Case, _Config) -> - persistent_term:erase(emqx_license_test_pubkey), + emqx_license_test_lib:unmock_parser(), ok. set_special_configs(emqx_license) -> diff --git a/apps/emqx_license/test/emqx_license_http_api_SUITE.erl b/apps/emqx_license/test/emqx_license_http_api_SUITE.erl index 4ee0c8c8e..3aa54feef 100644 --- a/apps/emqx_license/test/emqx_license_http_api_SUITE.erl +++ b/apps/emqx_license/test/emqx_license_http_api_SUITE.erl @@ -19,6 +19,7 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> + emqx_license_test_lib:mock_parser(), _ = application:load(emqx_conf), emqx_config:save_schema_mod_and_names(emqx_license_schema), emqx_common_test_helpers:start_apps([emqx_license, emqx_dashboard], fun set_special_configs/1), @@ -31,7 +32,7 @@ end_per_suite(_) -> emqx_config:put([license], Config), RawConfig = #{<<"key">> => LicenseKey}, emqx_config:put_raw([<<"license">>], RawConfig), - persistent_term:erase(emqx_license_test_pubkey), + emqx_license_test_lib:unmock_parser(), ok. set_special_configs(emqx_dashboard) -> @@ -48,10 +49,6 @@ set_special_configs(emqx_license) -> <<"connection_high_watermark">> => <<"80%">> }, emqx_config:put_raw([<<"license">>], RawConfig), - ok = persistent_term:put( - emqx_license_test_pubkey, - emqx_license_test_lib:public_key_pem() - ), ok; set_special_configs(_) -> ok. @@ -113,6 +110,19 @@ t_license_info(_Config) -> ), ok. +t_set_default_license(_Config) -> + NewKey = <<"default">>, + Res = request( + post, + uri(["license"]), + #{key => NewKey} + ), + ?assertMatch({ok, 200, _}, Res), + {ok, 200, Payload} = Res, + %% assert that it's not the string "default" returned + ?assertMatch(#{<<"customer">> := _}, emqx_utils_json:decode(Payload, [return_maps])), + ok. + t_license_upload_key_success(_Config) -> NewKey = emqx_license_test_lib:make_license(#{max_connections => "999"}), Res = request( diff --git a/apps/emqx_license/test/emqx_license_test_lib.erl b/apps/emqx_license/test/emqx_license_test_lib.erl index de5e597ea..c2f6c01e6 100644 --- a/apps/emqx_license/test/emqx_license_test_lib.erl +++ b/apps/emqx_license/test/emqx_license_test_lib.erl @@ -70,3 +70,13 @@ default_test_license() -> default_license() -> emqx_license_schema:default_license(). + +mock_parser() -> + meck:new(emqx_license_parser, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_license_parser, pubkey, fun() -> public_key_pem() end), + meck:expect(emqx_license_parser, default, fun() -> default_test_license() end), + ok. + +unmock_parser() -> + meck:unload(emqx_license_parser), + ok. diff --git a/rel/i18n/emqx_license_schema.hocon b/rel/i18n/emqx_license_schema.hocon index d17f717d3..e280af257 100644 --- a/rel/i18n/emqx_license_schema.hocon +++ b/rel/i18n/emqx_license_schema.hocon @@ -25,15 +25,16 @@ connection_low_watermark_field_deprecated.label: """deprecated use /license/setting instead""" key_field.desc: -"""This configuration parameter is designated for the license key and supports two input formats: +"""This configuration parameter is designated for the license key and supports below input formats: - Direct Key: Enter the secret key directly as a string value. - File Path: Specify the path to a file that contains the secret key. Ensure the path starts with file://. +- "default": Use string value "default" to apply the default trial license. Note: An invalid license key or an incorrect file path may prevent EMQX from starting successfully. -If a file path is used, EMQX attempts to reload the license key every 2 minutes. -Any failure in reloading the license key will be recorded as an error level log message, -without causing system downtime.""" +If a file path is used, EMQX attempts to reload the license key from the file every 2 minutes. +Any failure in reloading the license file will be recorded as an error level log message, +and EMQX continues to apply the license loaded previously.""" key_field.label: """License string"""