Merge pull request #12016 from zmstone/1122-load-license-file

1122 load license file
This commit is contained in:
Zaiming (Stone) Shi 2023-11-27 16:43:51 +01:00 committed by GitHub
commit d933d0b9e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 370 additions and 110 deletions

View File

@ -5,12 +5,15 @@
-module(emqx_license_checker).
-include("emqx_license.hrl").
-include_lib("emqx/include/logger.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-behaviour(gen_server).
-define(CHECK_INTERVAL, 5000).
-define(EXPIRY_ALARM_CHECK_INTERVAL, 24 * 60 * 60).
-define(CHECK_INTERVAL, timer:seconds(5)).
-define(REFRESH_INTERVAL, timer:minutes(2)).
-define(EXPIRY_ALARM_CHECK_INTERVAL, timer:hours(24)).
-define(OK(EXPR),
try
_ = begin
@ -56,7 +59,7 @@ start_link(LicenseFetcher) ->
start_link(LicenseFetcher, CheckInterval) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [LicenseFetcher, CheckInterval], []).
-spec update(emqx_license_parser:license()) -> ok.
-spec update(emqx_license_parser:license()) -> map().
update(License) ->
gen_server:call(?MODULE, {update, License}, infinity).
@ -94,15 +97,18 @@ init([LicenseFetcher, CheckInterval]) ->
check_license_interval => CheckInterval,
license => License
}),
State = ensure_check_expiry_timer(State0),
State1 = ensure_refresh_timer(State0),
State = ensure_check_expiry_timer(State1),
{ok, State};
{error, Reason} ->
{stop, Reason}
end.
handle_call({update, License}, _From, State) ->
handle_call({update, License}, _From, #{license := Old} = State) ->
ok = expiry_early_alarm(License),
{reply, check_license(License), State#{license => License}};
State1 = ensure_refresh_timer(State),
ok = log_new_license(Old, License),
{reply, check_license(License), State1#{license => License}};
handle_call(dump, _From, #{license := License} = State) ->
{reply, emqx_license_parser:dump(License), State};
handle_call(purge, _From, State) ->
@ -123,6 +129,10 @@ handle_info(check_expiry_alarm, #{license := License} = State) ->
ok = expiry_early_alarm(License),
NewState = ensure_check_expiry_timer(State),
{noreply, NewState};
handle_info(refresh, State0) ->
State1 = refresh(State0),
NewState = ensure_refresh_timer(State1),
{noreply, NewState};
handle_info(_Msg, State) ->
{noreply, State}.
@ -130,22 +140,59 @@ handle_info(_Msg, State) ->
%% Private functions
%%------------------------------------------------------------------------------
refresh(#{license := #{source := <<"file://", _/binary>> = Source} = License} = State) ->
case emqx_license_parser:parse(Source) of
{ok, License} ->
?tp(emqx_license_refresh_no_change, #{}),
%% no change
State;
{ok, NewLicense} ->
ok = log_new_license(License, NewLicense),
%% ensure alarm is set or cleared
ok = expiry_early_alarm(NewLicense),
?tp(emqx_license_refresh_changed, #{new_license => NewLicense}),
State#{license => NewLicense};
{error, Reason} ->
?tp(
error,
emqx_license_refresh_failed,
Reason#{continue_with_license => emqx_license_parser:summary(License)}
),
State
end;
refresh(State) ->
State.
log_new_license(Old, New) ->
?SLOG(info, #{
msg => "new_license_loaded",
old_license => emqx_license_parser:summary(Old),
new_license => emqx_license_parser:summary(New)
}).
ensure_check_license_timer(#{check_license_interval := CheckInterval} = State) ->
cancel_timer(State, timer),
State#{timer => erlang:send_after(CheckInterval, self(), check_license)}.
ok = cancel_timer(State, check_timer),
State#{check_timer => erlang:send_after(CheckInterval, self(), check_license)}.
ensure_check_expiry_timer(State) ->
cancel_timer(State, expiry_alarm_timer),
ok = cancel_timer(State, expiry_alarm_timer),
Ref = erlang:send_after(?EXPIRY_ALARM_CHECK_INTERVAL, self(), check_expiry_alarm),
State#{expiry_alarm_timer => Ref}.
%% refresh is to work with file:// license keys.
ensure_refresh_timer(State) ->
ok = cancel_timer(State, refresh_timer),
Ref = erlang:send_after(?REFRESH_INTERVAL, self(), refresh),
State#{refresh_timer => Ref}.
cancel_timer(State, Key) ->
_ =
case maps:find(Key, State) of
{ok, Ref} when is_reference(Ref) -> erlang:cancel_timer(Ref);
_ -> ok
end,
ok.
case maps:find(Key, State) of
{ok, Ref} when is_reference(Ref) ->
_ = erlang:cancel_timer(Ref),
ok;
_ ->
ok
end.
check_license(License) ->
DaysLeft = days_left(License),

View File

@ -54,7 +54,6 @@ schema("/license") ->
)
}
},
%% TODO(5.x): It's a update action, should use PUT instead
post => #{
tags => ?LICENSE_TAGS,
summary => <<"Update license key">>,

View File

@ -32,7 +32,14 @@
-type license_type() :: ?OFFICIAL | ?TRIAL.
-type license() :: #{module := module(), data := license_data()}.
-type license() :: #{
%% the parser module which parsed the license
module := module(),
%% the parse result
data := license_data(),
%% the source of the license, e.g. "file://path/to/license/file" or "******" for license key
source := binary()
}.
-export_type([
license_data/0,
@ -45,12 +52,19 @@
parse/1,
parse/2,
dump/1,
summary/1,
customer_type/1,
license_type/1,
expiry_date/1,
max_connections/1
]).
%% for testing purpose
-export([
default/0,
pubkey/0
]).
%%--------------------------------------------------------------------
%% Behaviour
%%--------------------------------------------------------------------
@ -59,6 +73,9 @@
-callback dump(license_data()) -> list({atom(), term()}).
%% provide a summary map for logging purposes
-callback summary(license_data()) -> map().
-callback customer_type(license_data()) -> customer_type().
-callback license_type(license_data()) -> license_type().
@ -71,19 +88,37 @@
%% API
%%--------------------------------------------------------------------
-ifdef(TEST).
-spec parse(string() | binary()) -> {ok, license()} | {error, term()}.
parse(Content) ->
PubKey = persistent_term:get(emqx_license_test_pubkey, ?PUBKEY),
parse(Content, PubKey).
-else.
-spec parse(string() | binary()) -> {ok, license()} | {error, term()}.
parse(Content) ->
parse(Content, ?PUBKEY).
-endif.
pubkey() -> ?PUBKEY.
default() -> emqx_license_schema:default_license().
parse(Content, Pem) ->
[PemEntry] = public_key:pem_decode(Pem),
%% @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(default | string() | binary()) -> {ok, license()} | {error, map()}.
parse(Content) ->
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} ->
case parse(Content, PubKey) of
{ok, License} ->
{ok, License#{source => FileKey}};
{error, Reason} ->
{error, Reason#{
license_file => Path
}}
end;
{error, Reason} ->
{error, #{
license_file => Path,
read_error => Reason
}}
end;
parse(Content, PubKey) ->
[PemEntry] = public_key:pem_decode(PubKey),
Key = public_key:pem_entry_decode(PemEntry),
do_parse(iolist_to_binary(Content), Key, ?LICENSE_PARSE_MODULES, []).
@ -91,6 +126,10 @@ parse(Content, Pem) ->
dump(#{module := Module, data := LicenseData}) ->
Module:dump(LicenseData).
-spec summary(license()) -> map().
summary(#{module := Module, data := Data}) ->
Module:summary(Data).
-spec customer_type(license()) -> customer_type().
customer_type(#{module := Module, data := LicenseData}) ->
Module:customer_type(LicenseData).
@ -112,14 +151,21 @@ max_connections(#{module := Module, data := LicenseData}) ->
%%--------------------------------------------------------------------
do_parse(_Content, _Key, [], Errors) ->
{error, lists:reverse(Errors)};
{error, #{parse_results => lists:reverse(Errors)}};
do_parse(Content, Key, [Module | Modules], Errors) ->
try Module:parse(Content, Key) of
{ok, LicenseData} ->
{ok, #{module => Module, data => LicenseData}};
{ok, #{module => Module, data => LicenseData, source => <<"******">>}};
{error, Error} ->
do_parse(Content, Key, Modules, [{Module, Error} | Errors])
do_parse(Content, Key, Modules, [#{module => Module, error => Error} | Errors])
catch
_Class:Error:Stacktrace ->
do_parse(Content, Key, Modules, [{Module, {Error, Stacktrace}} | Errors])
do_parse(Content, Key, Modules, [
#{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).

View File

@ -21,6 +21,7 @@
-export([
parse/2,
dump/1,
summary/1,
customer_type/1,
license_type/1,
expiry_date/1,
@ -69,6 +70,21 @@ dump(
{expiry, Expiry}
].
summary(
#{
deployment := Deployment,
date_start := DateStart,
max_connections := MaxConns
} = License
) ->
DateExpiry = expiry_date(License),
#{
deployment => Deployment,
max_connections => MaxConns,
start_at => format_date(DateStart),
expiry_at => format_date(DateExpiry)
}.
customer_type(#{customer_type := CType}) -> CType.
license_type(#{type := Type}) -> Type.
@ -85,17 +101,27 @@ max_connections(#{max_connections := MaxConns}) ->
%% Private functions
%%------------------------------------------------------------------------------
do_parse(Content) ->
do_parse(Content0) ->
try
[EncodedPayload, EncodedSignature] = binary:split(Content, <<".">>),
Payload = base64:decode(EncodedPayload),
Signature = base64:decode(EncodedSignature),
{ok, {Payload, Signature}}
Content = normalize(Content0),
do_parse2(Content)
catch
_:_ ->
{error, bad_license_format}
end.
do_parse2(<<>>) ->
{error, empty_string};
do_parse2(Content) ->
[EncodedPayload, EncodedSignature] = binary:split(Content, <<".">>),
Payload = base64:decode(EncodedPayload),
Signature = base64:decode(EncodedSignature),
{ok, {Payload, Signature}}.
%% drop whitespaces and newlines (CRLF)
normalize(Bin) ->
<<<<C>> || <<C>> <= Bin, C =/= $\s andalso C =/= $\n andalso C =/= $\r>>.
verify_signature(Payload, Signature, Key) ->
public_key:verify(Payload, ?DIGEST_TYPE, Signature, Key).
@ -182,7 +208,7 @@ collect_fields(Fields) ->
{FieldValues, []} ->
{ok, maps:from_list(FieldValues)};
{_, Errors} ->
{error, lists:reverse(Errors)}
{error, maps:from_list(Errors)}
end.
format_date({Year, Month, Day}) ->

View File

@ -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,

View File

@ -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.
@ -146,11 +138,11 @@ assert_on_nodes(Nodes, RunFun, CheckFun) ->
t_update_value(_Config) ->
?assertMatch(
{error, [_ | _]},
{error, #{parse_results := [_ | _]}},
emqx_license:update_key("invalid.license")
),
LicenseValue = emqx_license_test_lib:default_license(),
LicenseValue = emqx_license_test_lib:default_test_license(),
?assertMatch(
{ok, #{}},

View File

@ -14,12 +14,14 @@
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
init_per_suite(CtConfig) ->
_ = application:load(emqx_conf),
emqx_license_test_lib:mock_parser(),
ok = emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
Config.
CtConfig.
end_per_suite(_) ->
emqx_license_test_lib:unmock_parser(),
ok = emqx_common_test_helpers:stop_apps([emqx_license]).
init_per_testcase(t_default_limits, Config) ->
@ -35,7 +37,7 @@ end_per_testcase(_Case, _Config) ->
ok.
set_special_configs(emqx_license) ->
Config = #{key => emqx_license_test_lib:default_license()},
Config = #{key => emqx_license_test_lib:default_test_license()},
emqx_config:put([license], Config);
set_special_configs(_) ->
ok.
@ -100,7 +102,7 @@ t_update(_Config) ->
emqx_license_checker:limits()
).
t_update_by_timer(_Config) ->
t_check_by_timer(_Config) ->
?check_trace(
begin
?wait_async_action(
@ -228,10 +230,111 @@ t_unknown_calls(_Config) ->
some_msg = erlang:send(emqx_license_checker, some_msg),
?assertEqual(unknown, gen_server:call(emqx_license_checker, some_request)).
t_refresh_no_change(Config) when is_list(Config) ->
{ok, License} = write_test_license(Config, ?FUNCTION_NAME, 1, 111),
#{} = emqx_license_checker:update(License),
?check_trace(
begin
?wait_async_action(
begin
erlang:send(
emqx_license_checker,
refresh
)
end,
#{?snk_kind := emqx_license_refresh_no_change},
1000
)
end,
fun(Trace) ->
?assertMatch([_ | _], ?of_kind(emqx_license_refresh_no_change, Trace))
end
).
t_refresh_change(Config) when is_list(Config) ->
{ok, License} = write_test_license(Config, ?FUNCTION_NAME, 1, 111),
#{} = emqx_license_checker:update(License),
{ok, License2} = write_test_license(Config, ?FUNCTION_NAME, 2, 222),
?check_trace(
begin
?wait_async_action(
begin
erlang:send(
emqx_license_checker,
refresh
)
end,
#{?snk_kind := emqx_license_refresh_changed},
1000
)
end,
fun(Trace) ->
?assertMatch(
[#{new_license := License2} | _], ?of_kind(emqx_license_refresh_changed, Trace)
)
end
).
t_refresh_failure(Config) when is_list(Config) ->
Filename = test_license_file_name(Config, ?FUNCTION_NAME),
{ok, License} = write_test_license(Config, ?FUNCTION_NAME, 1, 111),
Summary = emqx_license_parser:summary(License),
#{} = emqx_license_checker:update(License),
ok = file:write_file(Filename, <<"invalid license">>),
?check_trace(
begin
?wait_async_action(
begin
erlang:send(
emqx_license_checker,
refresh
)
end,
#{?snk_kind := emqx_license_refresh_failed},
1000
)
end,
fun(Trace) ->
?assertMatch(
[#{continue_with_license := Summary} | _],
?of_kind(emqx_license_refresh_failed, Trace)
)
end
).
%%------------------------------------------------------------------------------
%% Tests
%%------------------------------------------------------------------------------
write_test_license(Config, Name, ExpireInDays, Connections) ->
{NowDate, _} = calendar:universal_time(),
DateTomorrow = calendar:gregorian_days_to_date(
calendar:date_to_gregorian_days(NowDate) + ExpireInDays
),
Fields = [
"220111",
"1",
"0",
"Foo",
"contact@foo.com",
"bar",
format_date(DateTomorrow),
"1",
integer_to_list(Connections)
],
FileName = test_license_file_name(Config, Name),
ok = write_license_file(FileName, Fields),
emqx_license_parser:parse(<<"file://", FileName/binary>>).
test_license_file_name(Config, Name) ->
Dir = ?config(data_dir, Config),
iolist_to_binary(filename:join(Dir, atom_to_list(Name) ++ ".lic")).
write_license_file(FileName, Fields) ->
EncodedLicense = emqx_license_test_lib:make_license(Fields),
ok = filelib:ensure_dir(FileName),
ok = file:write_file(FileName, EncodedLicense).
mk_license(Fields) ->
EncodedLicense = emqx_license_test_lib:make_license(Fields),
{ok, License} = emqx_license_parser:parse(

View File

@ -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) ->

View File

@ -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(

View File

@ -40,6 +40,7 @@ set_special_configs(_) ->
%%------------------------------------------------------------------------------
t_parse(_Config) ->
Parser = emqx_license_parser_v20220101,
?assertMatch({ok, _}, emqx_license_parser:parse(sample_license(), public_key_pem())),
%% invalid version
@ -61,10 +62,7 @@ t_parse(_Config) ->
),
?assertMatch({error, _}, Res1),
{error, Err1} = Res1,
?assertEqual(
invalid_version,
proplists:get_value(emqx_license_parser_v20220101, Err1)
),
?assertMatch(#{error := invalid_version}, find_error(Parser, Err1)),
%% invalid field number
Res2 = emqx_license_parser:parse(
@ -87,9 +85,9 @@ t_parse(_Config) ->
),
?assertMatch({error, _}, Res2),
{error, Err2} = Res2,
?assertEqual(
unexpected_number_of_fields,
proplists:get_value(emqx_license_parser_v20220101, Err2)
?assertMatch(
#{error := unexpected_number_of_fields},
find_error(Parser, Err2)
),
Res3 = emqx_license_parser:parse(
@ -110,14 +108,17 @@ t_parse(_Config) ->
),
?assertMatch({error, _}, Res3),
{error, Err3} = Res3,
?assertEqual(
[
{type, invalid_license_type},
{customer_type, invalid_customer_type},
{date_start, invalid_date},
{days, invalid_int_value}
],
proplists:get_value(emqx_license_parser_v20220101, Err3)
?assertMatch(
#{
error :=
#{
type := invalid_license_type,
customer_type := invalid_customer_type,
date_start := invalid_date,
days := invalid_int_value
}
},
find_error(Parser, Err3)
),
Res4 = emqx_license_parser:parse(
@ -139,14 +140,17 @@ t_parse(_Config) ->
?assertMatch({error, _}, Res4),
{error, Err4} = Res4,
?assertEqual(
[
{type, invalid_license_type},
{customer_type, invalid_customer_type},
{date_start, invalid_date},
{days, invalid_int_value}
],
proplists:get_value(emqx_license_parser_v20220101, Err4)
?assertMatch(
#{
error :=
#{
type := invalid_license_type,
customer_type := invalid_customer_type,
date_start := invalid_date,
days := invalid_int_value
}
},
find_error(Parser, Err4)
),
%% invalid signature
@ -189,14 +193,14 @@ t_parse(_Config) ->
),
?assertMatch({error, _}, Res5),
{error, Err5} = Res5,
?assertEqual(
invalid_signature,
proplists:get_value(emqx_license_parser_v20220101, Err5)
?assertMatch(
#{error := invalid_signature},
find_error(Parser, Err5)
),
%% totally invalid strings as license
?assertMatch(
{error, [_ | _]},
{error, #{parse_results := [#{error := bad_license_format}]}},
emqx_license_parser:parse(
<<"badlicense">>,
public_key_pem()
@ -204,7 +208,7 @@ t_parse(_Config) ->
),
?assertMatch(
{error, [_ | _]},
{error, #{parse_results := [#{error := bad_license_format}]}},
emqx_license_parser:parse(
<<"bad.license">>,
public_key_pem()
@ -249,6 +253,20 @@ t_expiry_date(_Config) ->
?assertEqual({2295, 10, 27}, emqx_license_parser:expiry_date(License)).
t_empty_string(_Config) ->
?assertMatch(
{error, #{
parse_results := [
#{
error := empty_string,
module := emqx_license_parser_v20220101
}
| _
]
}},
emqx_license_parser:parse(<<>>)
).
%%------------------------------------------------------------------------------
%% Helpers
%%------------------------------------------------------------------------------
@ -270,3 +288,10 @@ sample_license() ->
"10"
]
).
find_error(Module, #{parse_results := Results}) ->
find_error(Module, Results);
find_error(Module, [#{module := Module} = Result | _Results]) ->
Result;
find_error(Module, [_Result | Results]) ->
find_error(Module, Results).

View File

@ -7,17 +7,6 @@
-compile(nowarn_export_all).
-compile(export_all).
-define(DEFAULT_LICENSE_VALUES, [
"220111",
"0",
"10",
"Foo",
"contact@foo.com",
"20220111",
"100000",
"10"
]).
private_key() ->
test_key("pvt.key").
@ -76,5 +65,18 @@ make_license(Values) ->
EncodedSignature = base64:encode(Signature),
iolist_to_binary([EncodedText, ".", EncodedSignature]).
default_test_license() ->
make_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.

View File

@ -0,0 +1,4 @@
Enhanced license key management.
EMQX can now load the license key from a specified file. This is enabled by setting the `license.key` configuration to a file path, which should be prefixed with `"file://"`.
Also added the ability to revert to the default trial license by setting `license.key = default`. This option simplifies the process of returning to the trial license if needed.

View File

@ -25,7 +25,16 @@ connection_low_watermark_field_deprecated.label:
"""deprecated use /license/setting instead"""
key_field.desc:
"""License string"""
"""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 <code>file://</code>.
- "default": Use string value <code>"default"</code> 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 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"""