浏览代码

feat(license): add business critical customer type

zmstone 1 年之前
父节点
当前提交
ec83fbe3dc

+ 1 - 0
apps/emqx_license/include/emqx_license.hrl

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

+ 10 - 1
apps/emqx_license/src/emqx_license.erl

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

+ 39 - 10
apps/emqx_license/src/emqx_license_checker.erl

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

+ 12 - 1
apps/emqx_license/src/emqx_license_http_api.erl

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

+ 13 - 2
apps/emqx_license/src/emqx_license_parser.erl

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

+ 49 - 10
apps/emqx_license/src/emqx_license_schema.erl

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

+ 1 - 0
apps/emqx_license/test/emqx_license_cli_SUITE.erl

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

+ 40 - 7
apps/emqx_license/test/emqx_license_http_api_SUITE.erl

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

+ 7 - 12
rel/i18n/emqx_license_schema.hocon

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