feat(license): add business critical customer type
This commit is contained in:
parent
2a2fadfbff
commit
ec83fbe3dc
|
@ -31,6 +31,7 @@
|
|||
-define(SMALL_CUSTOMER, 0).
|
||||
-define(MEDIUM_CUSTOMER, 1).
|
||||
-define(LARGE_CUSTOMER, 2).
|
||||
-define(BUSINESS_CRITICAL_CUSTOMER, 3).
|
||||
-define(EVALUATION_CUSTOMER, 10).
|
||||
|
||||
-define(EXPIRED_DAY, -90).
|
||||
|
|
|
@ -154,7 +154,16 @@ do_update({key, Content}, Conf) when is_binary(Content); is_list(Content) ->
|
|||
{error, Reason} ->
|
||||
erlang:throw(Reason)
|
||||
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);
|
||||
do_update(NewConf, _PrevConf) ->
|
||||
#{<<"key">> := NewKey} = NewConf,
|
||||
|
|
|
@ -33,7 +33,9 @@
|
|||
expiry_epoch/0,
|
||||
purge/0,
|
||||
limits/0,
|
||||
print_warnings/1
|
||||
print_warnings/1,
|
||||
get_max_connections/1,
|
||||
get_dynamic_max_connections/0
|
||||
]).
|
||||
|
||||
%% gen_server callbacks
|
||||
|
@ -46,21 +48,23 @@
|
|||
|
||||
-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
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
-type limits() :: #{max_connections := non_neg_integer() | ?ERR_EXPIRED}.
|
||||
|
||||
-spec start_link(emqx_license_parser:license()) -> {ok, pid()}.
|
||||
-spec start_link(fetcher()) -> {ok, pid()}.
|
||||
start_link(LicenseFetcher) ->
|
||||
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) ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [LicenseFetcher, CheckInterval], []).
|
||||
|
||||
-spec update(emqx_license_parser:license()) -> map().
|
||||
-spec update(license()) -> map().
|
||||
update(License) ->
|
||||
gen_server:call(?MODULE, {update, License}, infinity).
|
||||
|
||||
|
@ -210,8 +214,7 @@ check_license(License) ->
|
|||
DaysLeft = days_left(License),
|
||||
IsOverdue = is_overdue(License, DaysLeft),
|
||||
NeedRestriction = IsOverdue,
|
||||
MaxConn = emqx_license_parser:max_connections(License),
|
||||
Limits = limits(License, NeedRestriction),
|
||||
#{max_connections := MaxConn} = Limits = limits(License, NeedRestriction),
|
||||
true = apply_limits(Limits),
|
||||
#{
|
||||
warn_evaluation => warn_evaluation(License, NeedRestriction, MaxConn),
|
||||
|
@ -223,8 +226,34 @@ warn_evaluation(License, false, MaxConn) ->
|
|||
warn_evaluation(_License, _NeedRestrict, _Limits) ->
|
||||
false.
|
||||
|
||||
limits(License, false) -> #{max_connections => emqx_license_parser:max_connections(License)};
|
||||
limits(_License, true) -> #{max_connections => ?ERR_EXPIRED}.
|
||||
limits(License, false) ->
|
||||
#{
|
||||
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) ->
|
||||
DateEnd = emqx_license_parser:expiry_date(License),
|
||||
|
|
|
@ -147,7 +147,7 @@ error_msg(Code, Msg) ->
|
|||
{400, error_msg(?BAD_REQUEST, <<"Invalid request params">>)}.
|
||||
|
||||
'/license/setting'(get, _Params) ->
|
||||
{200, maps:remove(<<"key">>, emqx_config:get_raw([license]))};
|
||||
{200, get_setting()};
|
||||
'/license/setting'(put, #{body := Setting}) ->
|
||||
case emqx_license:update_setting(Setting) of
|
||||
{error, Error} ->
|
||||
|
@ -170,3 +170,14 @@ fields(key_license) ->
|
|||
|
||||
setting() ->
|
||||
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.
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
?SMALL_CUSTOMER
|
||||
| ?MEDIUM_CUSTOMER
|
||||
| ?LARGE_CUSTOMER
|
||||
| ?BUSINESS_CRITICAL_CUSTOMER
|
||||
| ?EVALUATION_CUSTOMER.
|
||||
|
||||
-type license_type() :: ?OFFICIAL | ?TRIAL.
|
||||
|
@ -41,6 +42,8 @@
|
|||
source := binary()
|
||||
}.
|
||||
|
||||
-type raw_license() :: string() | binary() | default.
|
||||
|
||||
-export_type([
|
||||
license_data/0,
|
||||
customer_type/0,
|
||||
|
@ -56,7 +59,8 @@
|
|||
customer_type/1,
|
||||
license_type/1,
|
||||
expiry_date/1,
|
||||
max_connections/1
|
||||
max_connections/1,
|
||||
is_business_critical/1
|
||||
]).
|
||||
|
||||
%% for testing purpose
|
||||
|
@ -94,7 +98,7 @@ 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(default | string() | binary()) -> {ok, license()} | {error, map()}.
|
||||
-spec parse(raw_license()) -> {ok, license()} | {error, map()}.
|
||||
parse(Content) ->
|
||||
parse(to_bin(Content), ?MODULE:pubkey()).
|
||||
|
||||
|
@ -146,6 +150,13 @@ expiry_date(#{module := Module, data := LicenseData}) ->
|
|||
max_connections(#{module := Module, data := 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
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
-export([namespace/0, roots/0, fields/1, validations/0, desc/1, tags/0]).
|
||||
|
||||
-export([
|
||||
default_license/0
|
||||
default_license/0,
|
||||
default_setting/0
|
||||
]).
|
||||
|
||||
namespace() -> "license".
|
||||
|
@ -45,16 +46,26 @@ fields(key_license) ->
|
|||
required => true,
|
||||
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, #{
|
||||
type => emqx_schema:percent(),
|
||||
default => <<"75%">>,
|
||||
example => <<"75%">>,
|
||||
default => default(connection_low_watermark),
|
||||
example => default(connection_low_watermark),
|
||||
desc => ?DESC(connection_low_watermark_field)
|
||||
}},
|
||||
{connection_high_watermark, #{
|
||||
type => emqx_schema:percent(),
|
||||
default => <<"80%">>,
|
||||
example => <<"80%">>,
|
||||
default => default(connection_high_watermark),
|
||||
example => default(connection_high_watermark),
|
||||
desc => ?DESC(connection_high_watermark_field)
|
||||
}}
|
||||
].
|
||||
|
@ -87,11 +98,39 @@ check_license_watermark(Conf) ->
|
|||
|
||||
%% @doc The default license key.
|
||||
%% This default license has 25 connections limit.
|
||||
%% Issued on 2023-12-08 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
|
||||
%% Issued on 2024-04-18 and valid for 5 years (1825 days)
|
||||
%%
|
||||
%% 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() ->
|
||||
<<
|
||||
"MjIwMTExCjAKMTAKRXZhbHVhdGlvbgpjb250YWN0QGVtcXguaW8KdHJpYWwKMjAyMzEyMDgKMTgyNQoyNQo=."
|
||||
"MEUCIE271MtH+4bb39OZKD4mvVkurwZ3LX44KUvuOxkbjQz2AiEAqL7BP44PMUS5z5SAN1M4y3v3h47J8qORAqcuetnyexw="
|
||||
"MjIwMTExCjAKMTAKRXZhbHVhdGlvbgpjb250YWN0QGVtcXguaW8KdHJpYWwKMjAyNDA0MTgKMTgyNQoyNQo="
|
||||
"."
|
||||
"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.
|
||||
|
|
|
@ -65,6 +65,7 @@ t_conf_update(_Config) ->
|
|||
#{
|
||||
connection_high_watermark => 0.5,
|
||||
connection_low_watermark => 0.45,
|
||||
dynamic_max_connections => 25,
|
||||
key => LicenseKey
|
||||
},
|
||||
emqx:get_config([license])
|
||||
|
|
|
@ -19,17 +19,16 @@ all() ->
|
|||
|
||||
init_per_suite(Config) ->
|
||||
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(
|
||||
[
|
||||
emqx,
|
||||
emqx_conf,
|
||||
{emqx_license, #{
|
||||
config => #{
|
||||
license => #{
|
||||
key => emqx_license_test_lib:make_license(#{max_connections => "100"}),
|
||||
connection_low_watermark => <<"75%">>,
|
||||
connection_high_watermark => <<"80%">>
|
||||
}
|
||||
license => LicenseConf
|
||||
}
|
||||
}},
|
||||
{emqx_dashboard,
|
||||
|
@ -50,7 +49,7 @@ init_per_testcase(_TestCase, Config) ->
|
|||
Config.
|
||||
|
||||
end_per_testcase(_TestCase, _Config) ->
|
||||
{ok, _} = reset_license(),
|
||||
ok = reset_license(),
|
||||
ok.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -70,7 +69,11 @@ default_license() ->
|
|||
emqx_license_test_lib:make_license(#{max_connections => "100"}).
|
||||
|
||||
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() ->
|
||||
?assertMatch(
|
||||
|
@ -224,6 +227,26 @@ t_license_setting(_Config) ->
|
|||
),
|
||||
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) ->
|
||||
?assertMatch({ok, 200, _}, Res),
|
||||
{ok, 200, Payload} = Res,
|
||||
|
@ -234,3 +257,13 @@ validate_setting(Res, ExpectLow, ExpectHigh) ->
|
|||
},
|
||||
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]).
|
||||
|
|
|
@ -12,17 +12,12 @@ connection_low_watermark_field.desc:
|
|||
connection_low_watermark_field.label:
|
||||
"""Connection low watermark"""
|
||||
|
||||
connection_high_watermark_field_deprecated.desc:
|
||||
"""deprecated use /license/setting instead"""
|
||||
|
||||
connection_high_watermark_field_deprecated.label:
|
||||
"""deprecated use /license/setting instead"""
|
||||
|
||||
connection_low_watermark_field_deprecated.desc:
|
||||
"""deprecated use /license/setting instead"""
|
||||
|
||||
connection_low_watermark_field_deprecated.label:
|
||||
"""deprecated use /license/setting instead"""
|
||||
dynamic_max_connections {
|
||||
label: "Dynamic Connections Limit"
|
||||
desc: """~
|
||||
Only applicable for "Business Critical" license type. This config sets the current allocation of license for the current cluster.
|
||||
This value cannot exceed the connections limit assigned in the license key."""
|
||||
}
|
||||
|
||||
key_field.desc:
|
||||
"""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.
|
||||
|
||||
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.
|
||||
|
||||
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"""
|
||||
|
|
Loading…
Reference in New Issue