feat(license): add business critical customer type

This commit is contained in:
zmstone 2024-04-18 16:58:41 +02:00
parent 2a2fadfbff
commit ec83fbe3dc
9 changed files with 172 additions and 43 deletions

View File

@ -31,6 +31,7 @@
-define(SMALL_CUSTOMER, 0). -define(SMALL_CUSTOMER, 0).
-define(MEDIUM_CUSTOMER, 1). -define(MEDIUM_CUSTOMER, 1).
-define(LARGE_CUSTOMER, 2). -define(LARGE_CUSTOMER, 2).
-define(BUSINESS_CRITICAL_CUSTOMER, 3).
-define(EVALUATION_CUSTOMER, 10). -define(EVALUATION_CUSTOMER, 10).
-define(EXPIRED_DAY, -90). -define(EXPIRED_DAY, -90).

View File

@ -154,7 +154,16 @@ do_update({key, Content}, Conf) when is_binary(Content); is_list(Content) ->
{error, Reason} -> {error, Reason} ->
erlang:throw(Reason) erlang:throw(Reason)
end; end;
do_update({setting, Setting}, Conf) -> do_update({setting, Setting0}, Conf) ->
#{<<"key">> := Key} = Conf,
%% only allow updating dynamic_max_connections when it's BUSINESS_CRITICAL
Setting =
case emqx_license_parser:is_business_critical(Key) of
true ->
Setting0;
false ->
maps:without([<<"dynamic_max_connections">>], Setting0)
end,
maps:merge(Conf, Setting); maps:merge(Conf, Setting);
do_update(NewConf, _PrevConf) -> do_update(NewConf, _PrevConf) ->
#{<<"key">> := NewKey} = NewConf, #{<<"key">> := NewKey} = NewConf,

View File

@ -33,7 +33,9 @@
expiry_epoch/0, expiry_epoch/0,
purge/0, purge/0,
limits/0, limits/0,
print_warnings/1 print_warnings/1,
get_max_connections/1,
get_dynamic_max_connections/0
]). ]).
%% gen_server callbacks %% gen_server callbacks
@ -46,21 +48,23 @@
-define(LICENSE_TAB, emqx_license). -define(LICENSE_TAB, emqx_license).
-type limits() :: #{max_connections := non_neg_integer() | ?ERR_EXPIRED}.
-type license() :: emqx_license_parser:license().
-type fetcher() :: fun(() -> {ok, license()} | {error, term()}).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% API %% API
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
-type limits() :: #{max_connections := non_neg_integer() | ?ERR_EXPIRED}. -spec start_link(fetcher()) -> {ok, pid()}.
-spec start_link(emqx_license_parser:license()) -> {ok, pid()}.
start_link(LicenseFetcher) -> start_link(LicenseFetcher) ->
start_link(LicenseFetcher, ?CHECK_INTERVAL). start_link(LicenseFetcher, ?CHECK_INTERVAL).
-spec start_link(emqx_license_parser:license(), timeout()) -> {ok, pid()}. -spec start_link(fetcher(), timeout()) -> {ok, pid()}.
start_link(LicenseFetcher, CheckInterval) -> start_link(LicenseFetcher, CheckInterval) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [LicenseFetcher, CheckInterval], []). gen_server:start_link({local, ?MODULE}, ?MODULE, [LicenseFetcher, CheckInterval], []).
-spec update(emqx_license_parser:license()) -> map(). -spec update(license()) -> map().
update(License) -> update(License) ->
gen_server:call(?MODULE, {update, License}, infinity). gen_server:call(?MODULE, {update, License}, infinity).
@ -210,8 +214,7 @@ check_license(License) ->
DaysLeft = days_left(License), DaysLeft = days_left(License),
IsOverdue = is_overdue(License, DaysLeft), IsOverdue = is_overdue(License, DaysLeft),
NeedRestriction = IsOverdue, NeedRestriction = IsOverdue,
MaxConn = emqx_license_parser:max_connections(License), #{max_connections := MaxConn} = Limits = limits(License, NeedRestriction),
Limits = limits(License, NeedRestriction),
true = apply_limits(Limits), true = apply_limits(Limits),
#{ #{
warn_evaluation => warn_evaluation(License, NeedRestriction, MaxConn), warn_evaluation => warn_evaluation(License, NeedRestriction, MaxConn),
@ -223,8 +226,34 @@ warn_evaluation(License, false, MaxConn) ->
warn_evaluation(_License, _NeedRestrict, _Limits) -> warn_evaluation(_License, _NeedRestrict, _Limits) ->
false. false.
limits(License, false) -> #{max_connections => emqx_license_parser:max_connections(License)}; limits(License, false) ->
limits(_License, true) -> #{max_connections => ?ERR_EXPIRED}. #{
max_connections => get_max_connections(License)
};
limits(_License, true) ->
#{
max_connections => ?ERR_EXPIRED
}.
%% @doc Return the max_connections limit defined in license.
%% For business-critical type, it returns the dynamic value set in config.
-spec get_max_connections(license()) -> non_neg_integer().
get_max_connections(License) ->
Max = emqx_license_parser:max_connections(License),
Dyn =
case emqx_license_parser:customer_type(License) of
?BUSINESS_CRITICAL_CUSTOMER ->
min(get_dynamic_max_connections(), Max);
_ ->
Max
end,
min(Max, Dyn).
%% @doc Get the dynamic max_connections limit set in config.
%% It's only meaningful for business-critical license.
-spec get_dynamic_max_connections() -> non_neg_integer().
get_dynamic_max_connections() ->
emqx_conf:get([license, dynamic_max_connections]).
days_left(License) -> days_left(License) ->
DateEnd = emqx_license_parser:expiry_date(License), DateEnd = emqx_license_parser:expiry_date(License),

View File

@ -147,7 +147,7 @@ error_msg(Code, Msg) ->
{400, error_msg(?BAD_REQUEST, <<"Invalid request params">>)}. {400, error_msg(?BAD_REQUEST, <<"Invalid request params">>)}.
'/license/setting'(get, _Params) -> '/license/setting'(get, _Params) ->
{200, maps:remove(<<"key">>, emqx_config:get_raw([license]))}; {200, get_setting()};
'/license/setting'(put, #{body := Setting}) -> '/license/setting'(put, #{body := Setting}) ->
case emqx_license:update_setting(Setting) of case emqx_license:update_setting(Setting) of
{error, Error} -> {error, Error} ->
@ -170,3 +170,14 @@ fields(key_license) ->
setting() -> setting() ->
lists:keydelete(key, 1, emqx_license_schema:fields(key_license)). lists:keydelete(key, 1, emqx_license_schema:fields(key_license)).
%% Drop dynamic_max_connections unless it's a BUSINESS_CRITICAL license.
get_setting() ->
#{<<"key">> := Key} = Raw = emqx_config:get_raw([license]),
Result = maps:remove(<<"key">>, Raw),
case emqx_license_parser:is_business_critical(Key) of
true ->
Result;
false ->
maps:remove(<<"dynamic_max_connections">>, Result)
end.

View File

@ -28,6 +28,7 @@
?SMALL_CUSTOMER ?SMALL_CUSTOMER
| ?MEDIUM_CUSTOMER | ?MEDIUM_CUSTOMER
| ?LARGE_CUSTOMER | ?LARGE_CUSTOMER
| ?BUSINESS_CRITICAL_CUSTOMER
| ?EVALUATION_CUSTOMER. | ?EVALUATION_CUSTOMER.
-type license_type() :: ?OFFICIAL | ?TRIAL. -type license_type() :: ?OFFICIAL | ?TRIAL.
@ -41,6 +42,8 @@
source := binary() source := binary()
}. }.
-type raw_license() :: string() | binary() | default.
-export_type([ -export_type([
license_data/0, license_data/0,
customer_type/0, customer_type/0,
@ -56,7 +59,8 @@
customer_type/1, customer_type/1,
license_type/1, license_type/1,
expiry_date/1, expiry_date/1,
max_connections/1 max_connections/1,
is_business_critical/1
]). ]).
%% for testing purpose %% for testing purpose
@ -94,7 +98,7 @@ default() -> emqx_license_schema:default_license().
%% @doc Parse license key. %% @doc Parse license key.
%% If the license key is prefixed with "file://path/to/license/file", %% If the license key is prefixed with "file://path/to/license/file",
%% then the license key is read from the file. %% then the license key is read from the file.
-spec parse(default | string() | binary()) -> {ok, license()} | {error, map()}. -spec parse(raw_license()) -> {ok, license()} | {error, map()}.
parse(Content) -> parse(Content) ->
parse(to_bin(Content), ?MODULE:pubkey()). parse(to_bin(Content), ?MODULE:pubkey()).
@ -146,6 +150,13 @@ expiry_date(#{module := Module, data := LicenseData}) ->
max_connections(#{module := Module, data := LicenseData}) -> max_connections(#{module := Module, data := LicenseData}) ->
Module:max_connections(LicenseData). Module:max_connections(LicenseData).
-spec is_business_critical(license() | raw_license()) -> boolean().
is_business_critical(#{module := Module, data := LicenseData}) ->
Module:customer_type(LicenseData) =:= ?BUSINESS_CRITICAL_CUSTOMER;
is_business_critical(Key) when is_binary(Key) ->
{ok, License} = parse(Key),
is_business_critical(License).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Private functions %% Private functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -16,7 +16,8 @@
-export([namespace/0, roots/0, fields/1, validations/0, desc/1, tags/0]). -export([namespace/0, roots/0, fields/1, validations/0, desc/1, tags/0]).
-export([ -export([
default_license/0 default_license/0,
default_setting/0
]). ]).
namespace() -> "license". namespace() -> "license".
@ -45,16 +46,26 @@ fields(key_license) ->
required => true, required => true,
desc => ?DESC(key_field) desc => ?DESC(key_field)
}}, }},
%% This feature is not made GA yet, hence hidden.
%% When license is issued to cutomer-type BUSINESS_CRITICAL (code 3)
%% This config is taken as the real max_connections limit.
{dynamic_max_connections, #{
type => non_neg_integer(),
default => default(dynamic_max_connections),
required => false,
importance => ?IMPORTANCE_HIDDEN,
desc => ?DESC(dynamic_max_connections)
}},
{connection_low_watermark, #{ {connection_low_watermark, #{
type => emqx_schema:percent(), type => emqx_schema:percent(),
default => <<"75%">>, default => default(connection_low_watermark),
example => <<"75%">>, example => default(connection_low_watermark),
desc => ?DESC(connection_low_watermark_field) desc => ?DESC(connection_low_watermark_field)
}}, }},
{connection_high_watermark, #{ {connection_high_watermark, #{
type => emqx_schema:percent(), type => emqx_schema:percent(),
default => <<"80%">>, default => default(connection_high_watermark),
example => <<"80%">>, example => default(connection_high_watermark),
desc => ?DESC(connection_high_watermark_field) desc => ?DESC(connection_high_watermark_field)
}} }}
]. ].
@ -87,11 +98,39 @@ check_license_watermark(Conf) ->
%% @doc The default license key. %% @doc The default license key.
%% This default license has 25 connections limit. %% This default license has 25 connections limit.
%% Issued on 2023-12-08 and valid for 5 years (1825 days) %% Issued on 2024-04-18 and valid for 5 years (1825 days)
%% NOTE: when updating a new key, the schema doc in emqx_license_schema.hocon %%
%% should be updated accordingly %% NOTE: when updating a new key, below should be updated accordingly:
%% - emqx_license_schema.hocon default connections limit
%% - default(dynamic_max_connections) return value
default_license() -> default_license() ->
<< <<
"MjIwMTExCjAKMTAKRXZhbHVhdGlvbgpjb250YWN0QGVtcXguaW8KdHJpYWwKMjAyMzEyMDgKMTgyNQoyNQo=." "MjIwMTExCjAKMTAKRXZhbHVhdGlvbgpjb250YWN0QGVtcXguaW8KdHJpYWwKMjAyNDA0MTgKMTgyNQoyNQo="
"MEUCIE271MtH+4bb39OZKD4mvVkurwZ3LX44KUvuOxkbjQz2AiEAqL7BP44PMUS5z5SAN1M4y3v3h47J8qORAqcuetnyexw=" "."
"MEUCICMWWkfrvyMwQaQAOXEsEcs+d6+5uXc1BDxR7j25fRy4AiEAmblQ4p+FFmdsvnKgcRRkv1zj7PExmZKVk3mVcxH3fgw="
>>. >>.
%% @doc Exported for testing
default_setting() ->
Keys =
[
connection_low_watermark,
connection_high_watermark,
dynamic_max_connections
],
maps:from_list(
lists:map(
fun(K) ->
{K, default(K)}
end,
Keys
)
).
default(connection_low_watermark) ->
<<"75%">>;
default(connection_high_watermark) ->
<<"80%">>;
default(dynamic_max_connections) ->
%Must match the value encoded in default license.
25.

View File

@ -65,6 +65,7 @@ t_conf_update(_Config) ->
#{ #{
connection_high_watermark => 0.5, connection_high_watermark => 0.5,
connection_low_watermark => 0.45, connection_low_watermark => 0.45,
dynamic_max_connections => 25,
key => LicenseKey key => LicenseKey
}, },
emqx:get_config([license]) emqx:get_config([license])

View File

@ -19,17 +19,16 @@ all() ->
init_per_suite(Config) -> init_per_suite(Config) ->
emqx_license_test_lib:mock_parser(), emqx_license_test_lib:mock_parser(),
Setting = emqx_license_schema:default_setting(),
Key = emqx_license_test_lib:make_license(#{max_connections => "100"}),
LicenseConf = maps:merge(#{key => Key}, Setting),
Apps = emqx_cth_suite:start( Apps = emqx_cth_suite:start(
[ [
emqx, emqx,
emqx_conf, emqx_conf,
{emqx_license, #{ {emqx_license, #{
config => #{ config => #{
license => #{ license => LicenseConf
key => emqx_license_test_lib:make_license(#{max_connections => "100"}),
connection_low_watermark => <<"75%">>,
connection_high_watermark => <<"80%">>
}
} }
}}, }},
{emqx_dashboard, {emqx_dashboard,
@ -50,7 +49,7 @@ init_per_testcase(_TestCase, Config) ->
Config. Config.
end_per_testcase(_TestCase, _Config) -> end_per_testcase(_TestCase, _Config) ->
{ok, _} = reset_license(), ok = reset_license(),
ok. ok.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -70,7 +69,11 @@ default_license() ->
emqx_license_test_lib:make_license(#{max_connections => "100"}). emqx_license_test_lib:make_license(#{max_connections => "100"}).
reset_license() -> reset_license() ->
emqx_license:update_key(default_license()). {ok, _} = emqx_license:update_key(default_license()),
Setting = emqx_license_schema:default_setting(),
Req = maps:from_list([{atom_to_binary(K), V} || {K, V} <- maps:to_list(Setting)]),
{ok, _} = emqx_license:update_setting(Req),
ok.
assert_untouched_license() -> assert_untouched_license() ->
?assertMatch( ?assertMatch(
@ -224,6 +227,26 @@ t_license_setting(_Config) ->
), ),
ok. ok.
t_license_setting_bc(_Config) ->
%% Create a BC license
Key = emqx_license_test_lib:make_license(#{customer_type => "3"}),
Res = request(post, uri(["license"]), #{key => Key}),
?assertMatch({ok, 200, _}, Res),
%% get
GetRes = request(get, uri(["license", "setting"]), []),
validate_setting(GetRes, <<"75%">>, <<"80%">>, 25),
%% update
Low = <<"50%">>,
High = <<"55%">>,
UpdateRes = request(put, uri(["license", "setting"]), #{
<<"connection_low_watermark">> => Low,
<<"connection_high_watermark">> => High,
<<"dynamic_max_connections">> => 26
}),
validate_setting(UpdateRes, Low, High, 26),
?assertEqual(26, emqx_config:get([license, dynamic_max_connections])),
ok.
validate_setting(Res, ExpectLow, ExpectHigh) -> validate_setting(Res, ExpectLow, ExpectHigh) ->
?assertMatch({ok, 200, _}, Res), ?assertMatch({ok, 200, _}, Res),
{ok, 200, Payload} = Res, {ok, 200, Payload} = Res,
@ -234,3 +257,13 @@ validate_setting(Res, ExpectLow, ExpectHigh) ->
}, },
emqx_utils_json:decode(Payload, [return_maps]) emqx_utils_json:decode(Payload, [return_maps])
). ).
validate_setting(Res, ExpectLow, ExpectHigh, DynMax) ->
?assertMatch({ok, 200, _}, Res),
{ok, 200, Payload} = Res,
#{
<<"connection_low_watermark">> := ExpectLow,
<<"connection_high_watermark">> := ExpectHigh,
<<"dynamic_max_connections">> := DynMax
} =
emqx_utils_json:decode(Payload, [return_maps]).

View File

@ -12,17 +12,12 @@ connection_low_watermark_field.desc:
connection_low_watermark_field.label: connection_low_watermark_field.label:
"""Connection low watermark""" """Connection low watermark"""
connection_high_watermark_field_deprecated.desc: dynamic_max_connections {
"""deprecated use /license/setting instead""" label: "Dynamic Connections Limit"
desc: """~
connection_high_watermark_field_deprecated.label: Only applicable for "Business Critical" license type. This config sets the current allocation of license for the current cluster.
"""deprecated use /license/setting instead""" This value cannot exceed the connections limit assigned in the license key."""
}
connection_low_watermark_field_deprecated.desc:
"""deprecated use /license/setting instead"""
connection_low_watermark_field_deprecated.label:
"""deprecated use /license/setting instead"""
key_field.desc: key_field.desc:
"""This configuration parameter is designated for the license key and supports below input formats: """This configuration parameter is designated for the license key and supports below input formats:
@ -43,7 +38,7 @@ license_root.desc:
"""Defines the EMQX Enterprise license. """Defines the EMQX Enterprise license.
EMQX Enterprise is initially provided with a default trial license. EMQX Enterprise is initially provided with a default trial license.
This license, issued in December 2023, is valid for a period of 5 years. This license, issued in April 2024, is valid for a period of 5 years.
It supports up to 25 concurrent connections, catering to early-stage development and testing needs. It supports up to 25 concurrent connections, catering to early-stage development and testing needs.
For deploying EMQX Enterprise in a production environment, a different license is required. You can apply for a production license by visiting https://www.emqx.com/apply-licenses/emqx?version=5""" For deploying EMQX Enterprise in a production environment, a different license is required. You can apply for a production license by visiting https://www.emqx.com/apply-licenses/emqx?version=5"""