Merge pull request #12016 from zmstone/1122-load-license-file
1122 load license file
This commit is contained in:
commit
d933d0b9e0
|
@ -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),
|
||||
|
|
|
@ -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">>,
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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}) ->
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, #{}},
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
|
@ -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"""
|
||||
|
|
Loading…
Reference in New Issue