Просмотр исходного кода

feat(license): add license application

Ilya Averyanov 4 лет назад
Родитель
Сommit
17599432d1
48 измененных файлов с 2302 добавлено и 106 удалено
  1. 2 0
      .github/workflows/elixir_apps_check.yaml
  2. 1 0
      apps/emqx/priv/bpapi.versions
  3. 6 1
      apps/emqx/test/emqx_static_checks.erl
  4. 8 0
      apps/emqx_conf/etc/emqx_conf.conf
  5. 7 1
      apps/emqx_conf/src/emqx_conf_app.erl
  6. 5 0
      apps/emqx_conf/src/emqx_conf_schema.erl
  7. 8 22
      apps/emqx_machine/src/emqx_machine_boot.erl
  8. 17 1
      apps/emqx_machine/test/emqx_machine_SUITE.erl
  9. 4 4
      bin/emqx
  10. 19 0
      lib-ee/emqx_enterprise_conf/.gitignore
  11. 3 0
      lib-ee/emqx_enterprise_conf/README.md
  12. 2 0
      lib-ee/emqx_enterprise_conf/rebar.config
  13. 14 0
      lib-ee/emqx_enterprise_conf/src/emqx_enterprise_conf.app.src
  14. 32 0
      lib-ee/emqx_enterprise_conf/src/emqx_enterprise_conf_schema.erl
  15. 46 0
      lib-ee/emqx_enterprise_conf/test/emqx_enterprise_conf_schema_SUITE.erl
  16. 17 0
      lib-ee/emqx_license/.gitignore
  17. 3 0
      lib-ee/emqx_license/README.md
  18. 3 0
      lib-ee/emqx_license/etc/emqx_license.conf
  19. 38 0
      lib-ee/emqx_license/include/emqx_license.hrl
  20. 1 0
      lib-ee/emqx_license/rebar.config
  21. 7 0
      lib-ee/emqx_license/src/emqx_license.app.src
  22. 144 0
      lib-ee/emqx_license/src/emqx_license.erl
  23. 20 0
      lib-ee/emqx_license/src/emqx_license_app.erl
  24. 141 0
      lib-ee/emqx_license/src/emqx_license_checker.erl
  25. 77 0
      lib-ee/emqx_license/src/emqx_license_cli.erl
  26. 81 0
      lib-ee/emqx_license/src/emqx_license_installer.erl
  27. 114 0
      lib-ee/emqx_license/src/emqx_license_parser.erl
  28. 148 0
      lib-ee/emqx_license/src/emqx_license_parser_v20220101.erl
  29. 98 0
      lib-ee/emqx_license/src/emqx_license_resources.erl
  30. 27 0
      lib-ee/emqx_license/src/emqx_license_schema.erl
  31. 43 0
      lib-ee/emqx_license/src/emqx_license_sup.erl
  32. 24 0
      lib-ee/emqx_license/src/proto/emqx_license_proto_v1.erl
  33. 4 0
      lib-ee/emqx_license/test/data/pub.pem
  34. 9 0
      lib-ee/emqx_license/test/data/pvt.key
  35. 168 0
      lib-ee/emqx_license/test/emqx_license_SUITE.erl
  36. 208 0
      lib-ee/emqx_license/test/emqx_license_checker_SUITE.erl
  37. 72 0
      lib-ee/emqx_license/test/emqx_license_cli_SUITE.erl
  38. 81 0
      lib-ee/emqx_license/test/emqx_license_installer_SUITE.erl
  39. 197 0
      lib-ee/emqx_license/test/emqx_license_parser_SUITE.erl
  40. 85 0
      lib-ee/emqx_license/test/emqx_license_resources_SUITE.erl
  41. 50 0
      lib-ee/emqx_license/test/emqx_license_test_lib.erl
  42. 90 38
      mix.exs
  43. 68 26
      rebar.config.erl
  44. 1 1
      rel/emqx_vars
  45. 3 0
      scripts/check-deps-integrity.escript
  46. 3 3
      scripts/check-elixir-applications.exs
  47. 81 0
      scripts/check-elixir-emqx-machine-boot-discrepancies.exs
  48. 22 9
      scripts/merge-config.escript

+ 2 - 0
.github/workflows/elixir_apps_check.yaml

@@ -40,6 +40,8 @@ jobs:
         run: ./scripts/ensure-rebar3.sh 3.16.1-emqx-1
       - name: check applications
         run: ./scripts/check-elixir-applications.exs
+      - name: check applications started with emqx_machine
+        run: ./scripts/check-elixir-emqx-machine-boot-discrepancies.exs
         env:
           EMQX_RELEASE_TYPE: ${{ matrix.release_type }}
           EMQX_PACKAGE_TYPE: ${{ matrix.package_type }}

+ 1 - 0
apps/emqx/priv/bpapi.versions

@@ -6,6 +6,7 @@
 {emqx_dashboard,1}.
 {emqx_exhook,1}.
 {emqx_gateway_cm,1}.
+{emqx_license,1}.
 {emqx_management,1}.
 {emqx_mgmt_trace,1}.
 {emqx_persistent_session,1}.

+ 6 - 1
apps/emqx/test/emqx_static_checks.erl

@@ -32,12 +32,17 @@ end_per_suite(_Config) ->
           "If this test suite failed, and you are unsure why, read this:~n"
           "https://github.com/emqx/emqx/blob/master/apps/emqx/src/bpapi/README.md", []).
 
+check_if_versions_consistent(OldData, NewData) ->
+    %% OldData can contain a wider list of BPAPI versions
+    %% than the release being checked.
+    [] =:= NewData -- OldData.
+
 t_run_check(_) ->
     try
         {ok, OldData} = file:consult(emqx_bpapi_static_checks:versions_file()),
         ?assert(emqx_bpapi_static_checks:run()),
         {ok, NewData} = file:consult(emqx_bpapi_static_checks:versions_file()),
-        OldData =:= NewData orelse
+        check_if_versions_consistent(OldData, NewData) orelse
             begin
                 logger:critical(
                       "BPAPI versions were changed, but not committed to the repo.\n"

+ 8 - 0
apps/emqx_conf/etc/emqx_conf.conf

@@ -113,6 +113,14 @@ node {
   ## Default: 23
   backtrace_depth = 23
 
+  ## Comma-separated list of applications to start with emqx_machine.
+  ## These applications may restart on cluster leave/join.
+  ##
+  ## @doc node.applications
+  ## ValueType: String
+  ## Default: "gproc, esockd, ranch, cowboy, emqx"
+  applications = "{{ emqx_machine_boot_apps }}"
+
   cluster_call {
     retry_interval = 1s
     max_history = 100

+ 7 - 1
apps/emqx_conf/src/emqx_conf_app.erl

@@ -35,9 +35,15 @@ stop(_State) ->
 init_conf() ->
     {ok, TnxId} = copy_override_conf_from_core_node(),
     emqx_app:set_init_tnx_id(TnxId),
-    emqx_config:init_load(emqx_conf_schema),
+    emqx_config:init_load(schema_module()),
     emqx_app:set_init_config_load_done().
 
+schema_module() ->
+    case os:getenv("SCHEMA_MOD") of
+        false -> emqx_conf_schema;
+        Value -> list_to_existing_atom(Value)
+    end.
+
 copy_override_conf_from_core_node() ->
     case nodes() of
         [] -> %% The first core nodes is self.

+ 5 - 0
apps/emqx_conf/src/emqx_conf_schema.erl

@@ -306,6 +306,11 @@ a crash dump
           #{ mapping => "emqx_machine.backtrace_depth"
            , default => 23
            })}
+    , {"applications",
+       sc(emqx_schema:comma_separated_atoms(),
+          #{ mapping => "emqx_machine.applications"
+           , default => []
+           })}
     , {"etc_dir",
        sc(string(),
           #{ desc => "`etc` dir for the node"

+ 8 - 22
apps/emqx_machine/src/emqx_machine_boot.erl

@@ -26,6 +26,9 @@
 -export([sorted_reboot_apps/1]).
 -endif.
 
+%% these apps are always (re)started by emqx_machine
+-define(BASIC_REBOOT_APPS, [gproc, esockd, ranch, cowboy, emqx]).
+
 post_boot() ->
     ok = ensure_apps_started(),
     ok = print_vsn(),
@@ -80,29 +83,12 @@ start_one_app(App) ->
 %% list of app names which should be rebooted when:
 %% 1. due to static config change
 %% 2. after join a cluster
+
+%% the list of (re)started apps depends on release type/edition
+%% and is configured in rebar.config.erl/mix.exs
 reboot_apps() ->
-    [ gproc
-    , esockd
-    , ranch
-    , cowboy
-    , emqx
-    , emqx_prometheus
-    , emqx_modules
-    , emqx_dashboard
-    , emqx_connector
-    , emqx_gateway
-    , emqx_statsd
-    , emqx_resource
-    , emqx_rule_engine
-    , emqx_bridge
-    , emqx_plugin_libs
-    , emqx_management
-    , emqx_retainer
-    , emqx_exhook
-    , emqx_authn
-    , emqx_authz
-    , emqx_plugins
-    ].
+    {ok, Apps} = application:get_env(emqx_machine, applications),
+    ?BASIC_REBOOT_APPS ++ Apps.
 
 sorted_reboot_apps() ->
     Apps = [{App, app_deps(App)} || App <- reboot_apps()],

+ 17 - 1
apps/emqx_machine/test/emqx_machine_SUITE.erl

@@ -42,8 +42,24 @@ init_per_suite(Config) ->
     %%   Unload emqx_authz to avoid reboot this application
     %%
     application:unload(emqx_authz),
-
     emqx_common_test_helpers:start_apps([emqx_conf]),
+    application:set_env(emqx_machine, applications, [ emqx_prometheus
+                                                    , emqx_modules
+                                                    , emqx_dashboard
+                                                    , emqx_connector
+                                                    , emqx_gateway
+                                                    , emqx_statsd
+                                                    , emqx_resource
+                                                    , emqx_rule_engine
+                                                    , emqx_bridge
+                                                    , emqx_plugin_libs
+                                                    , emqx_management
+                                                    , emqx_retainer
+                                                    , emqx_exhook
+                                                    , emqx_authn
+                                                    , emqx_authz
+                                                    , emqx_plugin
+                                                    ]),
     Config.
 
 end_per_suite(_Config) ->

+ 4 - 4
bin/emqx

@@ -17,11 +17,11 @@ ROOT_DIR="$(cd "$(dirname "$(readlink "$0" || echo "$0")")"/..; pwd -P)"
 export RUNNER_ROOT_DIR
 export RUNNER_ETC_DIR
 export REL_VSN
+export SCHEMA_MOD
 
 RUNNER_SCRIPT="$RUNNER_BIN_DIR/$REL_NAME"
 CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}"
 REL_DIR="$RUNNER_ROOT_DIR/releases/$REL_VSN"
-SCHEMA_MOD=emqx_conf_schema
 
 WHOAMI=$(whoami)
 
@@ -389,7 +389,7 @@ generate_config() {
     ## meaning, certain overrides will not be mapped to app.<time>.config file
     ## disable SC2086 to allow EMQX_LICENSE_CONF_OPTION to split
     # shellcheck disable=SC2086
-    call_hocon -v -t "$NOW_TIME" -I "$CONFIGS_DIR/" -s $SCHEMA_MOD -c "$RUNNER_ETC_DIR"/emqx.conf $EMQX_LICENSE_CONF_OPTION -d "$RUNNER_DATA_DIR"/configs generate
+    call_hocon -v -t "$NOW_TIME" -I "$CONFIGS_DIR/" -s "$SCHEMA_MOD" -c "$RUNNER_ETC_DIR"/emqx.conf $EMQX_LICENSE_CONF_OPTION -d "$RUNNER_DATA_DIR"/configs generate
 
     ## filenames are per-hocon convention
     local CONF_FILE="$CONFIGS_DIR/app.$NOW_TIME.config"
@@ -539,7 +539,7 @@ NAME="${EMQX_NODE__NAME:-}"
 if [ -z "$NAME" ]; then
     if [ "$IS_BOOT_COMMAND" = 'yes' ]; then
         # for boot commands, inspect emqx.conf for node name
-        NAME="$(call_hocon -s $SCHEMA_MOD -I "$CONFIGS_DIR/" -c "$RUNNER_ETC_DIR"/emqx.conf get node.name | tr -d \")"
+        NAME="$(call_hocon -s "$SCHEMA_MOD" -I "$CONFIGS_DIR/" -c "$RUNNER_ETC_DIR"/emqx.conf get node.name | tr -d \")"
     else
         vm_args_file="$(latest_vm_args 'EMQX_NODE__NAME')"
         NAME="$(grep -E '^-s?name' "${vm_args_file}" | awk '{print $2}')"
@@ -570,7 +570,7 @@ fi
 COOKIE="${EMQX_NODE__COOKIE:-}"
 if [ -z "$COOKIE" ]; then
     if [ "$IS_BOOT_COMMAND" = 'yes' ]; then
-        COOKIE="$(call_hocon -s $SCHEMA_MOD -I "$CONFIGS_DIR/" -c "$RUNNER_ETC_DIR"/emqx.conf get node.cookie | tr -d \")"
+        COOKIE="$(call_hocon -s "$SCHEMA_MOD" -I "$CONFIGS_DIR/" -c "$RUNNER_ETC_DIR"/emqx.conf get node.cookie | tr -d \")"
     else
         vm_args_file="$(latest_vm_args 'EMQX_NODE__COOKIE')"
         COOKIE="$(grep -E '^-setcookie' "${vm_args_file}" | awk '{print $2}')"

+ 19 - 0
lib-ee/emqx_enterprise_conf/.gitignore

@@ -0,0 +1,19 @@
+.rebar3
+_*
+.eunit
+*.o
+*.beam
+*.plt
+*.swp
+*.swo
+.erlang.cookie
+ebin
+log
+erl_crash.dump
+.rebar
+logs
+_build
+.idea
+*.iml
+rebar3.crashdump
+*~

+ 3 - 0
lib-ee/emqx_enterprise_conf/README.md

@@ -0,0 +1,3 @@
+# emqx_enterprise_conf
+
+EMQ X Enterprise configuration schema

+ 2 - 0
lib-ee/emqx_enterprise_conf/rebar.config

@@ -0,0 +1,2 @@
+{erl_opts, [debug_info]}.
+{deps, []}.

+ 14 - 0
lib-ee/emqx_enterprise_conf/src/emqx_enterprise_conf.app.src

@@ -0,0 +1,14 @@
+{application, emqx_enterprise_conf,
+ [{description, "EMQ X Enterprise configuration schema"},
+  {vsn, "0.1.0"},
+  {registered, []},
+  {applications,
+   [kernel,
+    stdlib
+   ]},
+  {env,[]},
+  {modules, []},
+
+  {licenses, ["Apache 2.0"]},
+  {links, []}
+ ]}.

+ 32 - 0
lib-ee/emqx_enterprise_conf/src/emqx_enterprise_conf_schema.erl

@@ -0,0 +1,32 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_enterprise_conf_schema).
+
+-behaviour(hocon_schema).
+
+-export([namespace/0, roots/0, fields/1, translations/0, translation/1]).
+
+-define(EE_SCHEMA_MODULES, [emqx_license_schema
+                           ]).
+
+namespace() ->
+    emqx_conf_schema:namespace().
+
+roots() ->
+    lists:foldl(
+      fun(Module, Roots) ->
+              Roots ++ Module:roots()
+      end,
+      emqx_conf_schema:roots(),
+      ?EE_SCHEMA_MODULES).
+
+fields(Name) ->
+    emqx_conf_schema:fields(Name).
+
+translations() ->
+    emqx_conf_schema:translations().
+
+translation(Name) ->
+    emqx_conf_schema:translation(Name).

+ 46 - 0
lib-ee/emqx_enterprise_conf/test/emqx_enterprise_conf_schema_SUITE.erl

@@ -0,0 +1,46 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_enterprise_conf_schema_SUITE).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
+
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
+
+%%------------------------------------------------------------------------------
+%% Tests
+%%------------------------------------------------------------------------------
+
+t_namespace(_Config) ->
+    ?assertEqual(
+       emqx_conf_schema:namespace(),
+       emqx_enterprise_conf_schema:namespace()).
+
+t_roots(_Config) ->
+    BaseRoots = emqx_conf_schema:roots(),
+    EnterpriseRoots = emqx_enterprise_conf_schema:roots(),
+
+    ?assertEqual([], BaseRoots -- EnterpriseRoots),
+
+    ?assert(lists:any(
+              fun({license, _}) -> true;
+                 (_) -> false
+              end,
+              EnterpriseRoots)).
+
+t_fields(_Config) ->
+    ?assertEqual(
+       emqx_conf_schema:fields("node"),
+       emqx_enterprise_conf_schema:fields("node")).
+
+t_translations(_Config) ->
+    [Root | _] = emqx_enterprise_conf_schema:translations(),
+    ?assertEqual(
+       emqx_conf_schema:translation(Root),
+       emqx_enterprise_conf_schema:translation(Root)).

+ 17 - 0
lib-ee/emqx_license/.gitignore

@@ -0,0 +1,17 @@
+.eunit
+deps
+*.o
+*.beam
+*.plt
+erl_crash.dump
+ebin
+rel/example_project
+.concrete/DEV_MODE
+.rebar
+.DS_Store
+.erlang.mk/
+emqx_license.d
+erlang.mk
+_build/
+rebar.lock
+rebar3.crashdump

+ 3 - 0
lib-ee/emqx_license/README.md

@@ -0,0 +1,3 @@
+# emqx_license
+
+EMQ X 5.0 License Manager.

+ 3 - 0
lib-ee/emqx_license/etc/emqx_license.conf

@@ -0,0 +1,3 @@
+license {
+    key = "MjIwMTExCjAKMTAKRm9vCmNvbnRhY3RAZm9vLmNvbQoyMDIyMDExMQoxMDAwMDAKMTAK.Iyle9eMrXSAZwJczR8MEI2dtpxLuL2OKRikTwYvFK/SgxfwZQLR7JJM2rKfkuT5eP4cxh0Y1+84hOoB7fj/MWA=="
+}

+ 38 - 0
lib-ee/emqx_license/include/emqx_license.hrl

@@ -0,0 +1,38 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% @doc EMQ X License Management CLI.
+%%--------------------------------------------------------------------
+
+-ifndef(_EMQX_LICENSE_).
+-define(_EMQX_LICENSE_, true).
+
+-define(EVALUATION_LOG,
+    "\n"
+    "===============================================================================\n"
+    "This is an evaluation license that is restricted to 10 concurrent connections.\n"
+    "If you already have a paid license, please apply it now.\n"
+    "Or you could visit https://emqx.com/apply-licenses/emqx to get a trial license.\n"
+    "===============================================================================\n"
+    ).
+
+-define(EXPIRY_LOG,
+    "\n"
+    "======================================================\n"
+    "Your license has expired.\n"
+    "Please visit https://emqx.com/apply-licenses/emqx or\n"
+    "contact our customer services for an updated license.\n"
+    "======================================================\n"
+    ).
+
+-define(OFFICIAL, 1).
+-define(TRIAL, 0).
+
+-define(SMALL_CUSTOMER, 0).
+-define(MEDIUM_CUSTOMER, 1).
+-define(LARGE_CUSTOMER, 2).
+-define(EVALUATION_CUSTOMER, 10).
+
+-define(EXPIRED_DAY, -90).
+
+-endif.

+ 1 - 0
lib-ee/emqx_license/rebar.config

@@ -0,0 +1 @@
+{deps, [{emqx, {path, "../../apps/emqx"}}]}.

+ 7 - 0
lib-ee/emqx_license/src/emqx_license.app.src

@@ -0,0 +1,7 @@
+{application,emqx_license,
+             [{description,"EMQ X License"},
+              {vsn,"5.0.0"},
+              {modules,[]},
+              {registered,[emqx_license_sup]},
+              {applications,[kernel,stdlib]},
+              {mod,{emqx_license_app,[]}}]}.

+ 144 - 0
lib-ee/emqx_license/src/emqx_license.erl

@@ -0,0 +1,144 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+-module(emqx_license).
+
+-include_lib("emqx/include/logger.hrl").
+-include_lib("emqx/include/emqx_mqtt.hrl").
+-include_lib("typerefl/include/types.hrl").
+
+-behaviour(emqx_config_handler).
+
+-export([pre_config_update/3,
+         post_config_update/5
+        ]).
+
+-export([load/0,
+         check/2,
+         unload/0,
+         read_license/0,
+         update_file/1,
+         update_key/1]).
+
+-define(CONF_KEY_PATH, [license]).
+
+%%------------------------------------------------------------------------------
+%% API
+%%------------------------------------------------------------------------------
+
+-spec read_license() -> {ok, emqx_license_parser:license()} | {error, term()}.
+read_license() ->
+    read_license(emqx:get_config(?CONF_KEY_PATH)).
+
+-spec load() -> ok.
+load() ->
+    emqx_license_cli:load(),
+    emqx_conf:add_handler(?CONF_KEY_PATH, ?MODULE),
+    add_license_hook().
+
+-spec unload() -> ok.
+unload() ->
+    %% Delete the hook. This means that if the user calls
+    %% `application:stop(emqx_license).` from the shell, then here should no limitations!
+    del_license_hook(),
+    emqx_conf:remove_handler(?CONF_KEY_PATH),
+    emqx_license_cli:unload().
+
+-spec update_file(binary() | string()) ->
+    {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
+update_file(Filename) when is_binary(Filename); is_list(Filename) ->
+    Result = emqx_conf:update(
+               ?CONF_KEY_PATH,
+               {file, Filename},
+               #{rawconf_with_defaults => true, override_to => local}),
+    handle_config_update_result(Result).
+
+-spec update_key(binary() | string()) ->
+    {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
+update_key(Value) when is_binary(Value); is_list(Value) ->
+    Result = emqx_conf:update(
+               ?CONF_KEY_PATH,
+               {key, Value},
+               #{rawconf_with_defaults => true, override_to => cluster}),
+    handle_config_update_result(Result).
+
+%%------------------------------------------------------------------------------
+%% emqx_hooks
+%%------------------------------------------------------------------------------
+
+check(_ConnInfo, AckProps) ->
+    #{max_connections := MaxClients} = emqx_license_checker:limits(),
+    case MaxClients of
+        0 ->
+            ?SLOG(error, #{msg => "Connection rejected due to the license expiration"}),
+            {stop, {error, ?RC_QUOTA_EXCEEDED}};
+        _ ->
+            case check_max_clients_exceeded(MaxClients) of
+                true ->
+                    ?SLOG(error, #{msg => "Connection rejected due to max clients limitation"}),
+                    {stop, {error, ?RC_QUOTA_EXCEEDED}};
+                false ->
+                    {ok, AckProps}
+            end
+    end.
+
+%%------------------------------------------------------------------------------
+%% emqx_config_handler callbacks
+%%------------------------------------------------------------------------------
+
+pre_config_update(_, Cmd, Conf) ->
+    {ok, do_update(Cmd, Conf)}.
+
+post_config_update(_Path, _Cmd, NewConf, _Old, _AppEnvs) ->
+    case read_license(NewConf) of
+        {ok, License} ->
+            {ok, emqx_license_checker:update(License)};
+        {error, _} = Error -> Error
+    end.
+
+%%------------------------------------------------------------------------------
+%% Private functions
+%%------------------------------------------------------------------------------
+
+add_license_hook() ->
+    ok = emqx_hooks:put('client.connect', {?MODULE, check, []}).
+
+del_license_hook() ->
+    _ = emqx_hooks:del('client.connect', {?MODULE, check, []}),
+    ok.
+
+do_update({file, Filename}, _Conf) ->
+    case file:read_file(Filename) of
+        {ok, Content} ->
+            case emqx_license_parser:parse(Content) of
+                {ok, _License} ->
+                    #{<<"file">> => Filename};
+                {error, Reason} ->
+                    error(Reason)
+            end;
+        {error, Reason} ->
+            error({invalid_license_file, Reason})
+    end;
+
+do_update({key, Content}, _Conf) when is_binary(Content); is_list(Content) ->
+    case emqx_license_parser:parse(Content) of
+        {ok, _License} ->
+            #{<<"key">> => Content};
+        {error, Reason} ->
+            error(Reason)
+    end.
+
+check_max_clients_exceeded(MaxClients) ->
+    emqx_license_resources:connection_count() > MaxClients * 1.1.
+
+read_license(#{file := Filename}) ->
+    case file:read_file(Filename) of
+        {ok, Content} -> emqx_license_parser:parse(Content);
+        {error, _} = Error -> Error
+    end;
+
+read_license(#{key := Content}) ->
+    emqx_license_parser:parse(Content).
+
+handle_config_update_result({error, _} = Error) -> Error;
+handle_config_update_result({ok, #{post_config_update := #{emqx_license := Result}}}) -> {ok, Result}.

+ 20 - 0
lib-ee/emqx_license/src/emqx_license_app.erl

@@ -0,0 +1,20 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% @doc EMQ X License Management Application.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_app).
+
+-behaviour(application).
+
+-export([start/2, stop/1]).
+
+start(_Type, _Args) ->
+    ok = emqx_license:load(),
+    {ok, Sup} = emqx_license_sup:start_link(),
+    {ok, Sup}.
+
+stop(_State) ->
+    ok = emqx_license:unload(),
+    ok.

+ 141 - 0
lib-ee/emqx_license/src/emqx_license_checker.erl

@@ -0,0 +1,141 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_checker).
+
+-include("emqx_license.hrl").
+-include_lib("snabbkaffe/include/snabbkaffe.hrl").
+
+-behaviour(gen_server).
+
+-define(CHECK_INTERVAL, 5000).
+
+-export([start_link/1,
+         start_link/2,
+         update/1,
+         dump/0,
+         limits/0]).
+
+%% gen_server callbacks
+-export([init/1,
+         handle_call/3,
+         handle_cast/2,
+         handle_info/2]).
+
+%%------------------------------------------------------------------------------
+%% API
+%%------------------------------------------------------------------------------
+
+-type limits() :: #{max_connections := non_neg_integer()}.
+
+-spec start_link(emqx_license_parser:license()) -> {ok, pid()}.
+start_link(LicenseFetcher) ->
+    start_link(LicenseFetcher, ?CHECK_INTERVAL).
+
+-spec start_link(emqx_license_parser:license(), timeout()) -> {ok, pid()}.
+start_link(LicenseFetcher, CheckInterval) ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [LicenseFetcher, CheckInterval], []).
+
+-spec update(emqx_license_parser:license()) -> ok.
+update(License) ->
+    gen_server:call(?MODULE, {update, License}).
+
+-spec dump() -> [{atom(), term()}].
+dump() ->
+    gen_server:call(?MODULE, dump).
+
+-spec limits() -> limits().
+limits() ->
+    try ets:lookup(?MODULE, limits) of
+        [{limits, Limits}] -> Limits;
+        _ -> default_limits()
+    catch
+        error:badarg -> default_limits()
+    end.
+
+%%------------------------------------------------------------------------------
+%% gen_server callbacks
+%%------------------------------------------------------------------------------
+
+init([LicenseFetcher, CheckInterval]) ->
+    case LicenseFetcher() of
+        {ok, License} ->
+            _ = ets:new(?MODULE, [set, protected, named_table]),
+            #{} = check_license(License),
+            State = ensure_timer(#{check_license_interval => CheckInterval,
+                                   license => License}),
+            {ok, State};
+        {error, _} = Error ->
+            Error
+    end.
+
+handle_call({update, License}, _From, State) ->
+    {reply, check_license(License), State#{license => License}};
+
+handle_call(dump, _From, #{license := License} = State) ->
+    {reply, emqx_license_parser:dump(License), State};
+
+handle_call(_Req, _From, State) ->
+    {reply, unknown, State}.
+
+handle_cast(_Msg, State) ->
+    {noreply, State}.
+
+handle_info(check_license, #{license := License} = State) ->
+    #{} = check_license(License),
+    NewState = ensure_timer(State),
+    ?tp(debug, emqx_license_checked, #{}),
+    {noreply, NewState};
+
+handle_info(_Msg, State) ->
+    {noreply, State}.
+
+%%------------------------------------------------------------------------------
+%% Private functions
+%%------------------------------------------------------------------------------
+
+ensure_timer(#{check_license_interval := CheckInterval} = State) ->
+    _ = case State of
+            #{timer := Timer} -> erlang:cancel_timer(Timer);
+            _ -> ok
+        end,
+    State#{timer => erlang:send_after(CheckInterval, self(), check_license)}.
+
+check_license(License) ->
+    NeedRestrict = need_restrict(License),
+    Limits = limits(License, NeedRestrict),
+    true = apply_limits(Limits),
+    #{warn_evaluation => warn_evaluation(License, NeedRestrict),
+      warn_expiry => warn_expiry(License, NeedRestrict)}.
+
+warn_evaluation(License, false) ->
+    emqx_license_parser:customer_type(License) == ?EVALUATION_CUSTOMER;
+warn_evaluation(_License, _NeedRestrict) -> false.
+
+warn_expiry(_License, NeedRestrict) -> NeedRestrict.
+
+limits(License, false) -> #{max_connections => emqx_license_parser:max_connections(License)};
+limits(_License, true) -> #{max_connections => 0}.
+
+default_limits() -> #{max_connections => 0}.
+
+days_left(License) ->
+    DateEnd = emqx_license_parser:expiry_date(License),
+    {DateNow, _} = calendar:universal_time(),
+    calendar:date_to_gregorian_days(DateEnd) - calendar:date_to_gregorian_days(DateNow).
+
+need_restrict(License)->
+    DaysLeft = days_left(License),
+    CType = emqx_license_parser:customer_type(License),
+    Type = emqx_license_parser:license_type(License),
+
+    DaysLeft < 0
+    andalso (Type =/= ?OFFICIAL) or small_customer_overexpired(CType, DaysLeft).
+
+small_customer_overexpired(?SMALL_CUSTOMER, DaysLeft)
+    when DaysLeft < ?EXPIRED_DAY -> true;
+small_customer_overexpired(_CType, _DaysLeft) -> false.
+
+apply_limits(Limits) ->
+    ets:insert(?MODULE, {limits, Limits}).

+ 77 - 0
lib-ee/emqx_license/src/emqx_license_cli.erl

@@ -0,0 +1,77 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_cli).
+
+-include("emqx_license.hrl").
+
+-export([load/0, license/1, unload/0, print_warnings/1]).
+
+-define(PRINT_MSG(Msg), io:format(Msg)).
+
+-define(PRINT(Format, Args), io:format(Format, Args)).
+
+%%------------------------------------------------------------------------------
+%% API
+%%------------------------------------------------------------------------------
+
+load() ->
+    ok = emqx_ctl:register_command(license, {?MODULE, license}, []).
+
+license(["reload"]) ->
+    case emqx:get_config([license]) of
+        #{file := Filename} ->
+            license(["reload", Filename]);
+        #{key := _Key} ->
+            ?PRINT_MSG("License is not configured as a file, please specify file explicitly~n")
+    end;
+
+license(["reload", Filename]) ->
+    case emqx_license:update_file(Filename) of
+        {ok, Warnings} ->
+            ok = print_warnings(Warnings),
+            ok = ?PRINT_MSG("ok~n");
+        {error, Reason} -> ?PRINT("Error: ~p~n", [Reason])
+    end;
+
+license(["update", EncodedLicense]) ->
+    case emqx_license:update_key(EncodedLicense) of
+        {ok, Warnings} ->
+            ok = print_warnings(Warnings),
+            ok = ?PRINT_MSG("ok~n");
+        {error, Reason} -> ?PRINT("Error: ~p~n", [Reason])
+    end;
+
+license(["info"]) ->
+    lists:foreach(fun({K, V}) when is_binary(V); is_atom(V); is_list(V) ->
+                          ?PRINT("~-16s: ~s~n", [K, V]);
+                     ({K, V}) ->
+                          ?PRINT("~-16s: ~p~n", [K, V])
+                  end, emqx_license_checker:dump());
+
+license(_) ->
+    emqx_ctl:usage(
+      [ {"license info",            "Show license info"},
+        {"license reload [<File>]", "Reload license from a file specified with an absolute path"},
+        {"license update License",  "Update license given as a string"}
+      ]).
+
+unload() ->
+    ok = emqx_ctl:unregister_command(license).
+
+print_warnings(Warnings) ->
+    ok = print_evaluation_warning(Warnings),
+    ok = print_expiry_warning(Warnings).
+
+%%------------------------------------------------------------------------------
+%% Private functions
+%%------------------------------------------------------------------------------
+
+print_evaluation_warning(#{warn_evaluation := true}) ->
+    ?PRINT_MSG(?EVALUATION_LOG);
+print_evaluation_warning(_) -> ok.
+
+print_expiry_warning(#{warn_expiry := true}) ->
+    ?PRINT_MSG(?EXPIRY_LOG);
+print_expiry_warning(_) -> ok.

+ 81 - 0
lib-ee/emqx_license/src/emqx_license_installer.erl

@@ -0,0 +1,81 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+-module(emqx_license_installer).
+
+-include_lib("snabbkaffe/include/snabbkaffe.hrl").
+
+-behaviour(gen_server).
+
+-export([start_link/1,
+         start_link/4]).
+
+%% gen_server callbacks
+-export([init/1,
+         handle_call/3,
+         handle_cast/2,
+         handle_info/2]).
+
+-define(NAME, emqx).
+-define(INTERVAL, 5000).
+
+%%------------------------------------------------------------------------------
+%% API
+%%------------------------------------------------------------------------------
+
+start_link(Callback) ->
+    start_link(?NAME, ?MODULE, ?INTERVAL, Callback).
+
+start_link(Name, ServerName, Interval, Callback) ->
+    gen_server:start_link({local, ServerName}, ?MODULE, [Name, Interval, Callback], []).
+
+%%------------------------------------------------------------------------------
+%% gen_server callbacks
+%%------------------------------------------------------------------------------
+
+init([Name, Interval, Callback]) ->
+    Pid = whereis(Name),
+    State = #{interval => Interval,
+              name => Name,
+              pid => Pid,
+              callback => Callback
+             },
+    {ok, ensure_timer(State)}.
+
+handle_call(_Req, _From, State) ->
+    {reply, unknown, State}.
+
+handle_cast(_Msg, State) ->
+    {noreply, State}.
+
+handle_info({timeout, Timer, check_pid}, #{timer := Timer} = State) ->
+    NewState = check_pid(State),
+    {noreply, ensure_timer(NewState)};
+
+handle_info(_Msg, State) ->
+    {noreply, State}.
+
+%%------------------------------------------------------------------------------
+%% Private functions
+%%------------------------------------------------------------------------------
+
+ensure_timer(#{interval := Interval} = State) ->
+    _ = case State of
+            #{timer := Timer} -> erlang:cancel_timer(Timer);
+            _ -> ok
+        end,
+    State#{timer => erlang:start_timer(Interval, self(), check_pid)}.
+
+check_pid(#{name := Name, pid := OldPid, callback := Callback} = State) ->
+    case whereis(Name) of
+        undefined ->
+            ?tp(debug, emqx_license_installer_noproc, #{pid => OldPid}),
+            State;
+        OldPid ->
+            ?tp(debug, emqx_license_installer_nochange, #{pid => OldPid}),
+            State;
+        NewPid ->
+            _ = Callback(),
+            ?tp(debug, emqx_license_installer_called, #{pid => OldPid}),
+            State#{pid => NewPid}
+    end.

+ 114 - 0
lib-ee/emqx_license/src/emqx_license_parser.erl

@@ -0,0 +1,114 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% @doc EMQ X License Management.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_parser).
+
+-include_lib("emqx/include/logger.hrl").
+-include("emqx_license.hrl").
+
+-define(PUBKEY, <<"MEgCQQChzN6lCUdt4sYPQmWBYA3b8Zk87Jfk+1A1zcTd+lCU0Tf
+                  vXhSHgEWz18No4lL2v1n+70CoYpc2fzfhNJitgnV9AgMBAAE=">>).
+
+-define(LICENSE_PARSE_MODULES, [emqx_license_parser_v20220101
+                               ]).
+
+-type license_data() :: term().
+-type customer_type() :: ?SMALL_CUSTOMER |
+                         ?MEDIUM_CUSTOMER |
+                         ?LARGE_CUSTOMER |
+                         ?EVALUATION_CUSTOMER.
+
+-type license_type() :: ?OFFICIAL | ?TRIAL.
+
+-type license() :: #{module := module(), data := license_data()}.
+
+-export_type([license_data/0,
+              customer_type/0,
+              license_type/0,
+              license/0]).
+
+
+-export([parse/1,
+         parse/2,
+         dump/1,
+         customer_type/1,
+         license_type/1,
+         expiry_date/1,
+         max_connections/1
+        ]).
+
+-ifdef(TEST).
+-export([public_key/0
+        ]).
+-endif.
+
+%%--------------------------------------------------------------------
+%% Behaviour
+%%--------------------------------------------------------------------
+
+-callback parse(string() | binary(), binary()) -> {ok, license_data()} | {error, term()}.
+
+-callback dump(license_data()) -> list({atom(), term()}).
+
+-callback customer_type(license_data()) -> customer_type().
+
+-callback license_type(license_data()) -> license_type().
+
+-callback expiry_date(license_data()) -> calendar:date().
+
+-callback max_connections(license_data()) -> non_neg_integer().
+
+%%--------------------------------------------------------------------
+%% API
+%%--------------------------------------------------------------------
+
+-spec parse(string() | binary()) -> {ok, license()} | {error, term()}.
+parse(Content) ->
+    DecodedKey = base64:decode(public_key()),
+    parse(Content, DecodedKey).
+
+parse(Content, Key) ->
+    do_parse(iolist_to_binary(Content), Key, ?LICENSE_PARSE_MODULES, []).
+
+-spec dump(license()) -> list({atom(), term()}).
+dump(#{module := Module, data := LicenseData}) ->
+    Module:dump(LicenseData).
+
+-spec customer_type(license()) -> customer_type().
+customer_type(#{module := Module, data := LicenseData}) ->
+    Module:customer_type(LicenseData).
+
+-spec license_type(license()) -> license_type().
+license_type(#{module := Module, data := LicenseData}) ->
+    Module:license_type(LicenseData).
+
+-spec expiry_date(license()) -> calendar:date().
+expiry_date(#{module := Module, data := LicenseData}) ->
+    Module:expiry_date(LicenseData).
+
+-spec max_connections(license()) -> non_neg_integer().
+max_connections(#{module := Module, data := LicenseData}) ->
+    Module:max_connections(LicenseData).
+
+%%--------------------------------------------------------------------
+%% Private functions
+%%--------------------------------------------------------------------
+
+do_parse(_Content, _Key, [], Errors) ->
+    {error, {unknown_format, lists:reverse(Errors)}};
+
+do_parse(Content, Key, [Module | Modules], Errors) ->
+    try Module:parse(Content, Key) of
+        {ok, LicenseData} ->
+            {ok, #{module => Module, data => LicenseData}};
+        {error, Error} ->
+            do_parse(Content, Key, Modules, [{Module, Error} | Errors])
+    catch
+        _Class:Error:_Stk ->
+            do_parse(Content, Key, Modules, [{Module, Error} | Errors])
+    end.
+
+public_key() -> ?PUBKEY.

+ 148 - 0
lib-ee/emqx_license/src/emqx_license_parser_v20220101.erl

@@ -0,0 +1,148 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_parser_v20220101).
+
+-behaviour(emqx_license_parser).
+
+-include_lib("emqx/include/logger.hrl").
+-include("emqx_license.hrl").
+
+-define(DIGEST_TYPE, sha256).
+-define(LICENSE_VERSION, <<"220111">>).
+
+-export([parse/2,
+         dump/1,
+         customer_type/1,
+         license_type/1,
+         expiry_date/1,
+         max_connections/1]).
+
+%%------------------------------------------------------------------------------
+%% API
+%%------------------------------------------------------------------------------
+
+parse(Content, Key) ->
+    [EncodedPayload, EncodedSignature] = binary:split(Content, <<".">>),
+    Payload = base64:decode(EncodedPayload),
+    Signature = base64:decode(EncodedSignature),
+    case verify_signature(Payload, Signature, Key) of
+        true -> parse_payload(Payload);
+        false -> {error, invalid_signature}
+    end.
+
+dump(#{type := Type,
+       customer_type := CType,
+       customer := Customer,
+       email := Email,
+       date_start := DateStart,
+       max_connections := MaxConns} = License) ->
+
+    DateExpiry = expiry_date(License),
+    {DateNow, _} = calendar:universal_time(),
+    Expiry = DateNow > DateExpiry,
+
+    [{customer, Customer},
+     {email, Email},
+     {max_connections, MaxConns},
+     {start_at, format_date(DateStart)},
+     {expiry_at, format_date(DateExpiry)},
+     {type, format_type(Type)},
+     {customer_type, CType},
+     {expiry, Expiry}].
+
+customer_type(#{customer_type := CType}) -> CType.
+
+license_type(#{type := Type}) -> Type.
+
+expiry_date(#{date_start := DateStart, days := Days}) ->
+    calendar:gregorian_days_to_date(
+     calendar:date_to_gregorian_days(DateStart) + Days).
+
+max_connections(#{max_connections := MaxConns}) ->
+    MaxConns.
+
+%%------------------------------------------------------------------------------
+%% Private functions
+%%------------------------------------------------------------------------------
+
+verify_signature(Payload, Signature, Key) ->
+    RSAPublicKey = public_key:der_decode('RSAPublicKey', Key),
+    public_key:verify(Payload, ?DIGEST_TYPE, Signature, RSAPublicKey).
+
+parse_payload(Payload) ->
+    Lines = lists:map(
+              fun string:trim/1,
+              string:split(string:trim(Payload), <<"\n">>, all)),
+    case Lines of
+        [?LICENSE_VERSION, Type, CType, Customer, Email, DateStart, Days, MaxConns] ->
+            collect_fields([{type, parse_type(Type)},
+                            {customer_type, parse_customer_type(CType)},
+                            {customer, {ok, Customer}},
+                            {email, {ok, Email}},
+                            {date_start, parse_date_start(DateStart)},
+                            {days, parse_days(Days)},
+                            {max_connections, parse_max_connections(MaxConns)}]);
+        [_Version, _Type, _CType, _Customer, _Email, _DateStart, _Days, _MaxConns] ->
+            {error, invalid_version};
+        _ ->
+            {error, invalid_field_number}
+    end.
+
+parse_type(TypeStr) ->
+    case string:to_integer(TypeStr) of
+        {Type, <<"">>} -> {ok, Type};
+        _ -> {error, invalid_license_type}
+    end.
+
+parse_customer_type(CTypeStr) ->
+    case string:to_integer(CTypeStr) of
+        {CType, <<"">>} -> {ok, CType};
+        _ -> {error, invalid_customer_type}
+    end.
+
+parse_date_start(<<Y:4/binary, M:2/binary, D:2/binary>>) ->
+    Date = list_to_tuple([N || {N, <<>>} <- [string:to_integer(S) || S <- [Y, M, D]]]),
+    case calendar:valid_date(Date) of
+        true -> {ok, Date};
+        false -> {error, invalid_date}
+    end;
+parse_date_start(_) -> {error, invalid_date}.
+
+parse_days(DaysStr) ->
+    case string:to_integer(DaysStr) of
+        {Days, <<"">>} when Days > 0 -> {ok, Days};
+        _ -> {error, invalid_int_value}
+    end.
+
+parse_max_connections(MaxConnStr) ->
+    case string:to_integer(MaxConnStr) of
+        {MaxConns, <<"">>} when MaxConns > 0 -> {ok, MaxConns};
+        _ -> {error, invalid_int_value}
+    end.
+
+collect_fields(Fields) ->
+    Collected = lists:foldl(
+                  fun({Name, {ok, Value}}, {FieldValues, Errors}) ->
+                          {[{Name, Value} | FieldValues], Errors};
+                     ({Name, {error, Reason}}, {FieldValues, Errors}) ->
+                          {FieldValues, [{Name, Reason} | Errors]}
+                  end,
+                  {[], []},
+                  Fields),
+    case Collected of
+        {FieldValues, []} ->
+            {ok, maps:from_list(FieldValues)};
+        {_, Errors} ->
+            {error, lists:reverse(Errors)}
+    end.
+
+format_date({Year, Month, Day}) ->
+    iolist_to_binary(
+      io_lib:format(
+        "~4..0w-~2..0w-~2..0w",
+        [Year, Month, Day])).
+
+format_type(?OFFICIAL) -> <<"official">>;
+format_type(?TRIAL) -> <<"trial">>.

+ 98 - 0
lib-ee/emqx_license/src/emqx_license_resources.erl

@@ -0,0 +1,98 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_resources).
+
+-include_lib("snabbkaffe/include/snabbkaffe.hrl").
+
+-behaviour(gen_server).
+
+-define(CHECK_INTERVAL, 5000).
+
+-export([start_link/0,
+         start_link/1,
+         local_connection_count/0,
+         connection_count/0]).
+
+%% gen_server callbacks
+-export([init/1,
+         handle_call/3,
+         handle_cast/2,
+         handle_info/2,
+         terminate/2,
+         code_change/3]).
+
+%%------------------------------------------------------------------------------
+%% API
+%%------------------------------------------------------------------------------
+
+-spec start_link() -> {ok, pid()}.
+start_link() ->
+    start_link(?CHECK_INTERVAL).
+
+-spec start_link(timeout()) -> {ok, pid()}.
+start_link(CheckInterval) when is_integer(CheckInterval) ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [CheckInterval], []).
+
+-spec local_connection_count() -> non_neg_integer().
+local_connection_count() ->
+    emqx_cm:get_connected_client_count().
+
+-spec connection_count() -> non_neg_integer().
+connection_count() ->
+    local_connection_count() + cached_remote_connection_count().
+
+%%------------------------------------------------------------------------------
+%% gen_server callbacks
+%%------------------------------------------------------------------------------
+
+init([CheckInterval]) ->
+    _ = ets:new(?MODULE, [set, protected, named_table]),
+    State = ensure_timer(#{check_peer_interval => CheckInterval}),
+    {ok, State}.
+
+handle_call(_Req, _From, State) ->
+    {noreply, State}.
+
+handle_cast(_Msg, State) ->
+    {noreply, State}.
+
+handle_info(update_resources, State) ->
+    true = update_resources(),
+    ?tp(debug, emqx_license_resources_updated, #{}),
+    {noreply, ensure_timer(State)}.
+
+terminate(_Reason, _State) ->
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%%------------------------------------------------------------------------------
+%% Private functions
+%%------------------------------------------------------------------------------
+
+cached_remote_connection_count() ->
+    try ets:lookup(?MODULE, remote_connection_count) of
+        [{remote_connection_count, N}] -> N;
+        _ -> 0
+    catch
+        error:badarg -> 0
+    end.
+
+update_resources() ->
+    ets:insert(?MODULE, {remote_connection_count, remote_connection_count()}).
+
+ensure_timer(#{check_peer_interval := CheckInterval} = State) ->
+    _ = case State of
+            #{timer := Timer} -> erlang:cancel_timer(Timer);
+            _ -> ok
+        end,
+    State#{timer => erlang:send_after(CheckInterval, self(), update_resources)}.
+
+remote_connection_count() ->
+    Nodes = mria_mnesia:running_nodes() -- [node()],
+    Results = emqx_license_proto_v1:remote_connection_counts(Nodes),
+    Counts = [Count || {ok, Count} <- Results],
+    lists:sum(Counts).

+ 27 - 0
lib-ee/emqx_license/src/emqx_license_schema.erl

@@ -0,0 +1,27 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_schema).
+
+-include_lib("typerefl/include/types.hrl").
+
+%%------------------------------------------------------------------------------
+%% hocon_schema callbacks
+%%------------------------------------------------------------------------------
+
+-behaviour(hocon_schema).
+
+-export([roots/0, fields/1]).
+
+roots() -> [{license, hoconsc:union(
+                        [hoconsc:ref(?MODULE, key_license),
+                         hoconsc:ref(?MODULE, file_license)])}].
+
+fields(key_license) ->
+    [ {key, string()}
+    ];
+
+fields(file_license) ->
+    [ {file, string()}
+    ].

+ 43 - 0
lib-ee/emqx_license/src/emqx_license_sup.erl

@@ -0,0 +1,43 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% @doc EMQ X License Management Supervisor.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_sup).
+
+-behaviour(supervisor).
+
+-export([start_link/0]).
+
+-export([init/1]).
+
+start_link() ->
+	supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+    {ok, {#{strategy => one_for_one,
+            intensity => 10,
+            period => 100},
+
+          [#{id       => license_checker,
+             start    => {emqx_license_checker, start_link, [fun emqx_license:read_license/0]},
+             restart  => permanent,
+             shutdown => 5000,
+             type     => worker,
+             modules  => [emqx_license_checker]},
+
+           #{id       => license_resources,
+             start    => {emqx_license_resources, start_link, []},
+             restart  => permanent,
+             shutdown => 5000,
+             type     => worker,
+             modules  => [emqx_license_resources]},
+
+           #{id       => license_installer,
+             start    => {emqx_license_installer, start_link, [fun emqx_license:load/0]},
+             restart  => permanent,
+             shutdown => 5000,
+             type     => worker,
+             modules  => [emqx_license_installer]}
+          ]}}.

+ 24 - 0
lib-ee/emqx_license/src/proto/emqx_license_proto_v1.erl

@@ -0,0 +1,24 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_proto_v1).
+
+-behaviour(emqx_bpapi).
+
+-include_lib("emqx/include/bpapi.hrl").
+
+-export([ introduced_in/0
+        ]).
+
+-export([ remote_connection_counts/1
+        ]).
+
+-define(TIMEOUT, 500).
+
+introduced_in() ->
+    "5.0.0".
+
+-spec remote_connection_counts(list(node())) -> list({atom(), term()}).
+remote_connection_counts(Nodes) ->
+    erpc:multicall(Nodes, emqx_license_resources, local_connection_count, [], ?TIMEOUT).

+ 4 - 0
lib-ee/emqx_license/test/data/pub.pem

@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKHM3qUJR23ixg9CZYFgDdvxmTzsl+T7
+UDXNxN36UJTRN+9eFIeARbPXw2jiUva/Wf7vQKhilzZ/N+E0mK2CdX0CAwEAAQ==
+-----END PUBLIC KEY-----

+ 9 - 0
lib-ee/emqx_license/test/data/pvt.key

@@ -0,0 +1,9 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIBPAIBAAJBAKHM3qUJR23ixg9CZYFgDdvxmTzsl+T7UDXNxN36UJTRN+9eFIeA
+RbPXw2jiUva/Wf7vQKhilzZ/N+E0mK2CdX0CAwEAAQJBAJCy2UKbA8hgEGTBKmoD
+byGN9U8o/8aGgns7pJ4oKDyNWwM6Z3/omObDSTDcKn8Mfo26ccHUprIh+eiUW7TX
+F4ECIQDMfCREBKniVK1yDZgqKFe1+uZqj7ylT1DQne2S9bn2UQIhAMqP3TIAED3C
+MUfF3AN9oVDKJ/SFhQSKqI38XBmw9QVtAiEAqq801lHOPE3SOVF/ojDqhcxYaLpy
+DMqX+orYs8LI5wECIQC/5tuf6v94Aum9HW36wKJ7b4m61mPWkaZuHY8Dp+n5YQIg
+MrcXYujtNHEMWidC8S3ca1Ytp8kjMNcZVIil5CroP8E=
+-----END RSA PRIVATE KEY-----

+ 168 - 0
lib-ee/emqx_license/test/emqx_license_SUITE.erl

@@ -0,0 +1,168 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_SUITE).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-include_lib("emqx/include/emqx_mqtt.hrl").
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
+
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
+
+init_per_suite(Config) ->
+    _ = 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_common_test_helpers:stop_apps([emqx_license]),
+    ok.
+
+init_per_testcase(Case, Config) ->
+    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
+    meck:new(emqx_license_parser, [passthrough]),
+    meck:expect(emqx_license_parser, public_key, fun public_key/0),
+    set_invalid_license_file(Case),
+    Config.
+
+end_per_testcase(Case, _Config) ->
+    meck:unload(emqx_license_parser),
+    restore_valid_license_file(Case),
+    ok.
+
+set_invalid_license_file(t_read_license_from_invalid_file) ->
+    Config = #{file => "/invalid/file"},
+    emqx_config:put([license], Config);
+set_invalid_license_file(_) ->
+    ok.
+
+restore_valid_license_file(t_read_license_from_invalid_file) ->
+    Config = #{file => emqx_license_test_lib:default_license()},
+    emqx_config:put([license], Config);
+restore_valid_license_file(_) ->
+    ok.
+
+set_special_configs(emqx_license) ->
+    Config = #{file => emqx_license_test_lib:default_license()},
+    emqx_config:put([license], Config),
+    RawConfig = #{<<"file">> => emqx_license_test_lib:default_license()},
+    emqx_config:put_raw([<<"license">>], RawConfig);
+
+set_special_configs(_) -> ok.
+
+%%------------------------------------------------------------------------------
+%% Tests
+%%------------------------------------------------------------------------------
+
+t_update_file(_Config) ->
+    ?assertMatch(
+       {error, {invalid_license_file, enoent}},
+       emqx_license:update_file("/unknown/path")),
+
+    ok = file:write_file("license_with_invalid_content.lic", <<"bad license">>),
+    ?assertMatch(
+       {error, {unknown_format, _}},
+       emqx_license:update_file("license_with_invalid_content.lic")),
+
+    ?assertMatch(
+       {ok, #{}},
+       emqx_license:update_file(emqx_license_test_lib:default_license())).
+
+t_update_value(_Config) ->
+    ?assertMatch(
+       {error, {unknown_format, _}},
+       emqx_license:update_key("invalid.license")),
+
+    {ok, LicenseValue} = file:read_file(emqx_license_test_lib:default_license()),
+
+    ?assertMatch(
+       {ok, #{}},
+       emqx_license:update_key(LicenseValue)).
+
+t_read_license_from_invalid_file(_Config) ->
+    ?assertMatch(
+       {error, enoent},
+       emqx_license:read_license()).
+
+t_check_exceeded(_Config) ->
+    License = mk_license(
+                ["220111",
+                 "0",
+                 "10",
+                 "Foo",
+                 "contact@foo.com",
+                 "20220111",
+                 "100000",
+                 "10"]),
+    #{} = emqx_license_checker:update(License),
+
+    ok = lists:foreach(
+           fun(_) ->
+                   {ok, C} = emqtt:start_link(),
+                   {ok, _} = emqtt:connect(C)
+           end,
+           lists:seq(1, 12)),
+
+    ?assertEqual(
+       {stop, {error, ?RC_QUOTA_EXCEEDED}},
+       emqx_license:check(#{}, #{})).
+
+t_check_ok(_Config) ->
+    License = mk_license(
+                ["220111",
+                 "0",
+                 "10",
+                 "Foo",
+                 "contact@foo.com",
+                 "20220111",
+                 "100000",
+                 "10"]),
+    #{} = emqx_license_checker:update(License),
+
+    ok = lists:foreach(
+           fun(_) ->
+                   {ok, C} = emqtt:start_link(),
+                   {ok, _} = emqtt:connect(C)
+           end,
+           lists:seq(1, 11)),
+
+    ?assertEqual(
+       {ok, #{}},
+       emqx_license:check(#{}, #{})).
+
+t_check_expired(_Config) ->
+    License = mk_license(
+                ["220111",
+                 "1", %% Official customer
+                 "0", %% Small customer
+                 "Foo",
+                 "contact@foo.com",
+                 "20210101", %% Expired long ago
+                 "10",
+                 "10"]),
+    #{} = emqx_license_checker:update(License),
+
+    ?assertEqual(
+       {stop, {error, ?RC_QUOTA_EXCEEDED}},
+       emqx_license:check(#{}, #{})).
+
+%%------------------------------------------------------------------------------
+%% Helpers
+%%------------------------------------------------------------------------------
+
+mk_license(Fields) ->
+    EncodedLicense = emqx_license_test_lib:make_license(Fields),
+    {ok, License} = emqx_license_parser:parse(
+                      EncodedLicense,
+                      emqx_license_test_lib:public_key_encoded()),
+    License.
+
+public_key() -> <<"MEgCQQChzN6lCUdt4sYPQmWBYA3b8Zk87Jfk+1A1zcTd+lCU0Tf
+                  vXhSHgEWz18No4lL2v1n+70CoYpc2fzfhNJitgnV9AgMBAAE=">>.

+ 208 - 0
lib-ee/emqx_license/test/emqx_license_checker_SUITE.erl

@@ -0,0 +1,208 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_checker_SUITE).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
+-include_lib("snabbkaffe/include/snabbkaffe.hrl").
+
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
+
+init_per_suite(Config) ->
+    _ = application:load(emqx_conf),
+    ok = emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
+    Config.
+
+end_per_suite(_) ->
+    ok = emqx_common_test_helpers:stop_apps([emqx_license]).
+
+init_per_testcase(t_default_limits, Config) ->
+    ok = emqx_common_test_helpers:stop_apps([emqx_license]),
+    Config;
+
+init_per_testcase(_Case, Config) ->
+    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
+    Config.
+
+end_per_testcase(t_default_limits, _Config) ->
+    ok = emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1);
+
+end_per_testcase(_Case, _Config) ->
+    ok.
+
+set_special_configs(emqx_license) ->
+    Config = #{file => emqx_license_test_lib:default_license()},
+    emqx_config:put([license], Config);
+
+set_special_configs(_) -> ok.
+
+%%------------------------------------------------------------------------------
+%% Tests
+%%------------------------------------------------------------------------------
+
+t_default_limits(_Config) ->
+    ?assertMatch(#{max_connections := 0}, emqx_license_checker:limits()).
+
+t_dump(_Config) ->
+    License = mk_license(
+                ["220111",
+                 "0",
+                 "10",
+                 "Foo",
+                 "contact@foo.com",
+                 "20220111",
+                 "100000",
+                 "10"]),
+
+    #{} = emqx_license_checker:update(License),
+
+    ?assertEqual(
+       [{customer,<<"Foo">>},
+        {email,<<"contact@foo.com">>},
+        {max_connections,10},
+        {start_at,<<"2022-01-11">>},
+        {expiry_at,<<"2295-10-27">>},
+        {type,<<"trial">>},
+        {customer_type,10},
+        {expiry,false}],
+       emqx_license_checker:dump()).
+
+t_update(_Config) ->
+    License = mk_license(
+                ["220111",
+                 "0",
+                 "10",
+                 "Foo",
+                 "contact@foo.com",
+                 "20220111",
+                 "100000",
+                 "123"]),
+    #{} = emqx_license_checker:update(License),
+
+    ?assertMatch(
+       #{max_connections := 123},
+       emqx_license_checker:limits()).
+
+t_update_by_timer(_Config) ->
+    ?check_trace(
+       begin
+           ?wait_async_action(
+              begin
+                erlang:send(
+                    emqx_license_checker,
+                    check_license)
+              end,
+              #{?snk_kind := emqx_license_checked},
+              1000)
+       end,
+       fun(Trace) ->
+            ?assertMatch([_ | _], ?of_kind(emqx_license_checked, Trace))
+       end).
+
+t_expired_trial(_Config) ->
+    {NowDate, _} = calendar:universal_time(),
+    Date10DaysAgo = calendar:gregorian_days_to_date(
+                      calendar:date_to_gregorian_days(NowDate) - 10),
+
+    License = mk_license(
+                ["220111",
+                 "0",
+                 "10",
+                 "Foo",
+                 "contact@foo.com",
+                 format_date(Date10DaysAgo),
+                 "1",
+                 "123"]),
+    #{} = emqx_license_checker:update(License),
+
+    ?assertMatch(
+       #{max_connections := 0},
+       emqx_license_checker:limits()).
+
+t_overexpired_small_client(_Config) ->
+    {NowDate, _} = calendar:universal_time(),
+    Date100DaysAgo = calendar:gregorian_days_to_date(
+                      calendar:date_to_gregorian_days(NowDate) - 100),
+
+    License = mk_license(
+                ["220111",
+                 "1",
+                 "0",
+                 "Foo",
+                 "contact@foo.com",
+                 format_date(Date100DaysAgo),
+                 "1",
+                 "123"]),
+    #{} = emqx_license_checker:update(License),
+
+    ?assertMatch(
+       #{max_connections := 0},
+       emqx_license_checker:limits()).
+
+t_overexpired_medium_client(_Config) ->
+    {NowDate, _} = calendar:universal_time(),
+    Date100DaysAgo = calendar:gregorian_days_to_date(
+                      calendar:date_to_gregorian_days(NowDate) - 100),
+
+    License = mk_license(
+                ["220111",
+                 "1",
+                 "1",
+                 "Foo",
+                 "contact@foo.com",
+                 format_date(Date100DaysAgo),
+                 "1",
+                 "123"]),
+    #{} = emqx_license_checker:update(License),
+
+    ?assertMatch(
+       #{max_connections := 123},
+       emqx_license_checker:limits()).
+
+t_recently_expired_small_client(_Config) ->
+    {NowDate, _} = calendar:universal_time(),
+    Date10DaysAgo = calendar:gregorian_days_to_date(
+                      calendar:date_to_gregorian_days(NowDate) - 10),
+
+    License = mk_license(
+                ["220111",
+                 "1",
+                 "0",
+                 "Foo",
+                 "contact@foo.com",
+                 format_date(Date10DaysAgo),
+                 "1",
+                 "123"]),
+    #{} = emqx_license_checker:update(License),
+
+    ?assertMatch(
+       #{max_connections := 123},
+       emqx_license_checker:limits()).
+
+t_unknown_calls(_Config) ->
+    ok = gen_server:cast(emqx_license_checker, some_cast),
+    some_msg = erlang:send(emqx_license_checker, some_msg),
+    ?assertEqual(unknown, gen_server:call(emqx_license_checker, some_request)).
+
+%%------------------------------------------------------------------------------
+%% Tests
+%%------------------------------------------------------------------------------
+
+mk_license(Fields) ->
+    EncodedLicense = emqx_license_test_lib:make_license(Fields),
+    {ok, License} = emqx_license_parser:parse(
+                      EncodedLicense,
+                      emqx_license_test_lib:public_key_encoded()),
+    License.
+
+format_date({Year, Month, Day}) ->
+    lists:flatten(
+      io_lib:format(
+        "~4..0w~2..0w~2..0w",
+        [Year, Month, Day])).

+ 72 - 0
lib-ee/emqx_license/test/emqx_license_cli_SUITE.erl

@@ -0,0 +1,72 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_cli_SUITE).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
+
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
+
+init_per_suite(Config) ->
+    _ = 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_common_test_helpers:stop_apps([emqx_license]),
+    ok.
+
+init_per_testcase(_Case, Config) ->
+    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
+    meck:new(emqx_license_parser, [passthrough]),
+    meck:expect(emqx_license_parser, public_key, fun public_key/0),
+    Config.
+
+end_per_testcase(_Case, _Config) ->
+    meck:unload(emqx_license_parser),
+    ok.
+
+set_special_configs(emqx_license) ->
+    Config = #{file => emqx_license_test_lib:default_license()},
+    emqx_config:put([license], Config),
+    RawConfig = #{<<"file">> => emqx_license_test_lib:default_license()},
+    emqx_config:put_raw([<<"license">>], RawConfig);
+
+set_special_configs(_) -> ok.
+
+%%------------------------------------------------------------------------------
+%% Tests
+%%------------------------------------------------------------------------------
+
+t_help(_Config) ->
+    _ = emqx_license_cli:license([]).
+
+t_info(_Config) ->
+    _ = emqx_license_cli:license(["info"]).
+
+t_reload(_Config) ->
+    _ = emqx_license_cli:license(["reload", "/invalid/path"]),
+    _ = emqx_license_cli:license(["reload", emqx_license_test_lib:default_license()]),
+    _ = emqx_license_cli:license(["reload"]).
+
+t_update(_Config) ->
+    {ok, LicenseValue} = file:read_file(emqx_license_test_lib:default_license()),
+    _ = emqx_license_cli:license(["update", LicenseValue]),
+    _ = emqx_license_cli:license(["reload"]),
+    _ = emqx_license_cli:license(["update", "Invalid License Value"]).
+
+%%------------------------------------------------------------------------------
+%% Helpers
+%%------------------------------------------------------------------------------
+
+public_key() -> <<"MEgCQQChzN6lCUdt4sYPQmWBYA3b8Zk87Jfk+1A1zcTd+lCU0Tf
+                  vXhSHgEWz18No4lL2v1n+70CoYpc2fzfhNJitgnV9AgMBAAE=">>.
+
+digest() -> <<"3jHg0zCb4NL5v8eIoKn+CNDMq8A04mXEOefqlUBSSVs=">>.

+ 81 - 0
lib-ee/emqx_license/test/emqx_license_installer_SUITE.erl

@@ -0,0 +1,81 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_installer_SUITE).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
+-include_lib("snabbkaffe/include/snabbkaffe.hrl").
+
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
+
+init_per_suite(Config) ->
+    _ = application:load(emqx_conf),
+    emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
+    Config.
+
+end_per_suite(_) ->
+    emqx_common_test_helpers:stop_apps([emqx_license]),
+    ok.
+
+init_per_testcase(_Case, Config) ->
+    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
+    Config.
+
+end_per_testcase(_Case, _Config) ->
+    ok.
+
+set_special_configs(emqx_license) ->
+    Config = #{file => emqx_license_test_lib:default_license()},
+    emqx_config:put([license], Config);
+
+set_special_configs(_) -> ok.
+
+%%------------------------------------------------------------------------------
+%% Tests
+%%------------------------------------------------------------------------------
+
+t_update(_Config) ->
+    ?check_trace(
+       begin
+           ?wait_async_action(
+              begin
+                Pid0 = spawn_link(fun() -> receive exit -> ok end end),
+                register(installer_test, Pid0),
+
+                {ok, _} = emqx_license_installer:start_link(
+                           installer_test,
+                           ?MODULE,
+                           10,
+                           fun() -> ok end),
+
+
+                {ok, _} = ?block_until(
+                             #{?snk_kind := emqx_license_installer_nochange},
+                             100),
+
+                Pid0 ! exit,
+
+                {ok, _} = ?block_until(
+                             #{?snk_kind := emqx_license_installer_noproc},
+                             100),
+
+                Pid1 = spawn_link(fun() -> timer:sleep(100) end),
+                register(installer_test, Pid1)
+              end,
+              #{?snk_kind := emqx_license_installer_called},
+              1000)
+       end,
+       fun(Trace) ->
+            ?assertMatch([_ | _], ?of_kind(emqx_license_installer_called, Trace))
+       end).
+
+t_unknown_calls(_Config) ->
+    ok = gen_server:cast(emqx_license_installer, some_cast),
+    some_msg = erlang:send(emqx_license_installer, some_msg),
+    ?assertEqual(unknown, gen_server:call(emqx_license_installer, some_request)).

+ 197 - 0
lib-ee/emqx_license/test/emqx_license_parser_SUITE.erl

@@ -0,0 +1,197 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_parser_SUITE).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
+
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
+
+init_per_suite(Config) ->
+    _ = application:load(emqx_conf),
+    emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
+    Config.
+
+end_per_suite(_) ->
+    emqx_common_test_helpers:stop_apps([emqx_license]),
+    ok.
+
+init_per_testcase(_Case, Config) ->
+    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
+    Config.
+
+end_per_testcase(_Case, _Config) ->
+    ok.
+
+set_special_configs(emqx_license) ->
+    Config = #{file => emqx_license_test_lib:default_license()},
+    emqx_config:put([license], Config);
+
+set_special_configs(_) -> ok.
+
+%%------------------------------------------------------------------------------
+%% Tests
+%%------------------------------------------------------------------------------
+
+t_parse(_Config) ->
+    ?assertMatch({ok, _}, emqx_license_parser:parse(sample_license(), public_key_encoded())),
+
+    %% invalid version
+    ?assertMatch(
+       {error,
+        {unknown_format,
+         [{emqx_license_parser_v20220101,invalid_version}]}},
+       emqx_license_parser:parse(
+         emqx_license_test_lib:make_license(
+           ["220101",
+            "0",
+            "10",
+            "Foo",
+            "contact@foo.com",
+            "20220111",
+            "100000",
+            "10"
+           ]),
+         public_key_encoded())),
+
+    %% invalid field number
+    ?assertMatch(
+       {error,
+        {unknown_format,
+         [{emqx_license_parser_v20220101,invalid_field_number}]}},
+       emqx_license_parser:parse(
+         emqx_license_test_lib:make_license(
+           ["220111",
+            "0",
+            "10",
+            "Foo", "Bar",
+            "contact@foo.com",
+            "20220111",
+            "100000",
+            "10"
+           ]),
+         public_key_encoded())),
+
+    ?assertMatch(
+       {error,
+        {unknown_format,
+         [{emqx_license_parser_v20220101,
+           [{type,invalid_license_type},
+            {customer_type,invalid_customer_type},
+            {date_start,invalid_date},
+            {days,invalid_int_value}]}]}},
+       emqx_license_parser:parse(
+         emqx_license_test_lib:make_license(
+           ["220111",
+            "zero",
+            "ten",
+            "Foo",
+            "contact@foo.com",
+            "20220231",
+            "-10",
+            "10"
+           ]),
+         public_key_encoded())),
+
+    %% invalid signature
+    [LicensePart, _] = binary:split(
+                         emqx_license_test_lib:make_license(
+                           ["220111",
+                            "0",
+                            "10",
+                            "Foo",
+                            "contact@foo.com",
+                            "20220111",
+                            "100000",
+                            "10"]),
+                         <<".">>),
+    [_, SignaturePart] = binary:split(
+                           emqx_license_test_lib:make_license(
+                             ["220111",
+                              "1",
+                              "10",
+                              "Foo",
+                              "contact@foo.com",
+                              "20220111",
+                              "100000",
+                              "10"]),
+                           <<".">>),
+
+    ?assertMatch(
+       {error,
+        {unknown_format,
+         [{emqx_license_parser_v20220101,invalid_signature}]}},
+       emqx_license_parser:parse(
+         iolist_to_binary([LicensePart, <<".">>, SignaturePart]),
+         public_key_encoded())),
+
+    %% totally invalid strings as license
+    ?assertMatch(
+       {error, {unknown_format, _}},
+       emqx_license_parser:parse(
+         <<"badlicense">>,
+         public_key_encoded())),
+
+    ?assertMatch(
+       {error, {unknown_format, _}},
+       emqx_license_parser:parse(
+         <<"bad.license">>,
+         public_key_encoded())).
+
+t_dump(_Config) ->
+    {ok, License} = emqx_license_parser:parse(sample_license(), public_key_encoded()),
+
+    ?assertEqual(
+       [{customer,<<"Foo">>},
+        {email,<<"contact@foo.com">>},
+        {max_connections,10},
+        {start_at,<<"2022-01-11">>},
+        {expiry_at,<<"2295-10-27">>},
+        {type,<<"trial">>},
+        {customer_type,10},
+        {expiry,false}],
+       emqx_license_parser:dump(License)).
+
+t_customer_type(_Config) ->
+    {ok, License} = emqx_license_parser:parse(sample_license(), public_key_encoded()),
+
+    ?assertEqual(10, emqx_license_parser:customer_type(License)).
+
+t_license_type(_Config) ->
+    {ok, License} = emqx_license_parser:parse(sample_license(), public_key_encoded()),
+
+    ?assertEqual(0, emqx_license_parser:license_type(License)).
+
+t_max_connections(_Config) ->
+    {ok, License} = emqx_license_parser:parse(sample_license(), public_key_encoded()),
+
+    ?assertEqual(10, emqx_license_parser:max_connections(License)).
+
+t_expiry_date(_Config) ->
+    {ok, License} = emqx_license_parser:parse(sample_license(), public_key_encoded()),
+
+    ?assertEqual({2295,10,27}, emqx_license_parser:expiry_date(License)).
+
+%%------------------------------------------------------------------------------
+%% Helpers
+%%------------------------------------------------------------------------------
+
+public_key_encoded() ->
+    emqx_license_test_lib:public_key_encoded().
+
+sample_license() ->
+    emqx_license_test_lib:make_license(
+      ["220111",
+       "0",
+       "10",
+       "Foo",
+       "contact@foo.com",
+       "20220111",
+       "100000",
+       "10"]).

+ 85 - 0
lib-ee/emqx_license/test/emqx_license_resources_SUITE.erl

@@ -0,0 +1,85 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_resources_SUITE).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
+-include_lib("snabbkaffe/include/snabbkaffe.hrl").
+
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
+
+init_per_suite(Config) ->
+    _ = application:load(emqx_conf),
+    emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
+    Config.
+
+end_per_suite(_) ->
+    emqx_common_test_helpers:stop_apps([emqx_license]),
+    ok.
+
+init_per_testcase(_Case, Config) ->
+    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
+    Config.
+
+end_per_testcase(_Case, _Config) ->
+    ok.
+
+set_special_configs(emqx_license) ->
+    Config = #{file => emqx_license_test_lib:default_license()},
+    emqx_config:put([license], Config);
+
+set_special_configs(_) -> ok.
+
+%%------------------------------------------------------------------------------
+%% Tests
+%%------------------------------------------------------------------------------
+
+t_connection_count(_Config) ->
+    ?check_trace(
+       begin
+           ?wait_async_action(
+              whereis(emqx_license_resources) ! update_resources,
+              #{?snk_kind := emqx_license_resources_updated},
+              1000),
+           emqx_license_resources:connection_count()
+       end,
+       fun(ConnCount, Trace) ->
+               ?assertEqual(0, ConnCount),
+               ?assertMatch([_ | _], ?of_kind(emqx_license_resources_updated, Trace))
+       end),
+
+
+    meck:new(emqx_cm, [passthrough]),
+    meck:expect(emqx_cm, get_connected_client_count, fun() -> 10 end),
+
+    meck:new(emqx_license_proto_v1, [passthrough]),
+    meck:expect(
+      emqx_license_proto_v1,
+      remote_connection_counts,
+      fun(_Nodes) ->
+              [{ok, 5}, {error, some_error}]
+      end),
+
+    ?check_trace(
+       begin
+           ?wait_async_action(
+              whereis(emqx_license_resources) ! update_resources,
+              #{?snk_kind := emqx_license_resources_updated},
+              1000),
+           emqx_license_resources:connection_count()
+       end,
+       fun(ConnCount, _Trace) ->
+               ?assertEqual(15, ConnCount)
+       end),
+
+    meck:unload(emqx_license_proto_v1),
+    meck:unload(emqx_cm).
+
+t_emqx_license_proto(_Config) ->
+    ?assert("5.0.0" =< emqx_license_proto_v1:introduced_in()).

+ 50 - 0
lib-ee/emqx_license/test/emqx_license_test_lib.erl

@@ -0,0 +1,50 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_test_lib).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-define(DEFAULT_LICENSE_VALUES,
+        ["220111",
+         "0",
+         "10",
+         "Foo",
+         "contact@foo.com",
+         "20220111",
+         "100000",
+         "10"]).
+
+-define(DEFAULT_LICENSE_FILE, "emqx.lic").
+
+private_key() ->
+    test_key("pvt.key").
+
+public_key() ->
+    test_key("pub.pem").
+
+public_key_encoded() ->
+    public_key:der_encode('RSAPublicKey', public_key()).
+
+test_key(Filename) ->
+    Dir = code:lib_dir(emqx_license, test),
+    Path = filename:join([Dir, "data", Filename]),
+    {ok, KeyData} = file:read_file(Path),
+    [PemEntry] = public_key:pem_decode(KeyData),
+    Key = public_key:pem_entry_decode(PemEntry),
+    Key.
+
+make_license(Values) ->
+    Key = private_key(),
+    Text = string:join(Values, "\n"),
+    EncodedText = base64:encode(Text),
+    Signature = public_key:sign(Text, sha256, Key),
+    EncodedSignature = base64:encode(Signature),
+    iolist_to_binary([EncodedText, ".", EncodedSignature]).
+
+default_license() ->
+    License = make_license(?DEFAULT_LICENSE_VALUES),
+    ok = file:write_file(?DEFAULT_LICENSE_FILE, License),
+    ?DEFAULT_LICENSE_FILE.

+ 90 - 38
mix.exs

@@ -132,7 +132,7 @@ defmodule EMQXUmbrella.MixProject do
           end
 
         [
-          applications: applications(release_type),
+          applications: applications(release_type, edition_type),
           skip_mode_validation_for: [
             :emqx_gateway,
             :emqx_dashboard,
@@ -154,7 +154,7 @@ defmodule EMQXUmbrella.MixProject do
     ]
   end
 
-  def applications(release_type) do
+  def applications(release_type, edition_type) do
     [
       crypto: :permanent,
       public_key: :permanent,
@@ -200,12 +200,52 @@ defmodule EMQXUmbrella.MixProject do
     ] ++
       if(enable_quicer?(), do: [quicer: :permanent], else: []) ++
       if(enable_bcrypt?(), do: [bcrypt: :permanent], else: []) ++
+      if(edition_type == :enterprise,
+        do: [
+          emqx_enterprise_conf: :load,
+          emqx_license: :permanent
+        ],
+        else: []
+      ) ++
       if(release_type == :cloud,
         do: [xmerl: :permanent, observer: :load],
         else: []
       )
   end
 
+  def emqx_machine_boot_apps(:community) do
+    [
+      :emqx_prometheus,
+      :emqx_modules,
+      :emqx_dashboard,
+      :emqx_connector,
+      :emqx_gateway,
+      :emqx_statsd,
+      :emqx_resource,
+      :emqx_rule_engine,
+      :emqx_bridge,
+      :emqx_plugin_libs,
+      :emqx_management,
+      :emqx_retainer,
+      :emqx_exhook,
+      :emqx_authn,
+      :emqx_authz,
+      :emqx_plugin
+    ]
+  end
+
+  def emqx_machine_boot_apps(:enterprise) do
+    emqx_machine_boot_apps(:community) ++
+      []
+  end
+
+  defp emqx_machine_boot_app_list(edition_type) do
+    edition_type
+    |> emqx_machine_boot_apps()
+    |> Enum.map(&Atom.to_string/1)
+    |> Enum.join(", ")
+  end
+
   def check_profile!() do
     valid_envs = [
       :dev,
@@ -314,24 +354,24 @@ defmodule EMQXUmbrella.MixProject do
     # This is generated by `scripts/merge-config.escript` or `make
     # conf-segs`.  So, this should be run before the release.
     # TODO: run as a "compiler" step???
-    conf_rendered =
-      File.read!("apps/emqx_conf/etc/emqx.conf.all")
-      |> from_rebar_to_eex_template()
-      |> EEx.eval_string(assigns)
-
-    File.write!(
-      Path.join(etc, "emqx.conf"),
-      conf_rendered
+    render_template(
+      "apps/emqx_conf/etc/emqx.conf.all",
+      assigns,
+      Path.join(etc, "emqx.conf")
     )
 
-    vars_rendered =
-      File.read!("rel/emqx_vars")
-      |> from_rebar_to_eex_template()
-      |> EEx.eval_string(assigns)
+    if edition_type == :enterprise do
+      render_template(
+        "apps/emqx_conf/etc/emqx_enterprise.conf.all",
+        assigns,
+        Path.join(etc, "emqx_enterprise.conf")
+      )
+    end
 
-    File.write!(
-      Path.join([release.path, "releases", "emqx_vars"]),
-      vars_rendered
+    render_template(
+      "rel/emqx_vars",
+      assigns,
+      Path.join([release.path, "releases", "emqx_vars"])
     )
 
     vm_args_template_path =
@@ -343,19 +383,13 @@ defmodule EMQXUmbrella.MixProject do
           "apps/emqx/etc/emqx_edge/vm.args"
       end
 
-    vm_args_rendered =
-      File.read!(vm_args_template_path)
-      |> from_rebar_to_eex_template()
-      |> EEx.eval_string(assigns)
-
-    File.write!(
-      Path.join(etc, "vm.args"),
-      vm_args_rendered
-    )
-
-    File.write!(
-      Path.join(release.version_path, "vm.args"),
-      vm_args_rendered
+    render_template(
+      vm_args_template_path,
+      assigns,
+      [
+        Path.join(etc, "vm.args"),
+        Path.join(release.version_path, "vm.args")
+      ]
     )
 
     for name <- [
@@ -383,19 +417,30 @@ defmodule EMQXUmbrella.MixProject do
       File.chmod!(Path.join(bin, name), 0o755)
     end
 
-    built_on_rendered =
-      File.read!("rel/BUILT_ON")
-      |> from_rebar_to_eex_template()
-      |> EEx.eval_string(assigns)
-
-    File.write!(
-      Path.join([release.version_path, "BUILT_ON"]),
-      built_on_rendered
+    render_template(
+      "rel/BUILT_ON",
+      assigns,
+      Path.join(release.version_path, "BUILT_ON")
     )
 
     release
   end
 
+  defp render_template(template, assigns, target) when is_binary(target) do
+    render_template(template, assigns, [target])
+  end
+
+  defp render_template(template, assigns, tartgets) when is_list(tartgets) do
+    rendered =
+      File.read!(template)
+      |> from_rebar_to_eex_template()
+      |> EEx.eval_string(assigns)
+
+    for target <- tartgets do
+      File.write!(target, rendered)
+    end
+  end
+
   # needed by nodetool and by release_handler
   defp create_RELEASES(release) do
     apps =
@@ -487,6 +532,8 @@ defmodule EMQXUmbrella.MixProject do
       # FIXME: this is empty in `make emqx` ???
       erl_opts: "",
       emqx_description: emqx_description(release_type, edition_type),
+      emqx_schema_mod: emqx_schema_mod(edition_type),
+      emqx_machine_boot_apps: emqx_machine_boot_app_list(edition_type),
       built_on_arch: built_on(),
       is_elixir: "yes"
     ]
@@ -513,6 +560,8 @@ defmodule EMQXUmbrella.MixProject do
       erl_opts: "",
       emqx_description: emqx_description(release_type, edition_type),
       built_on_arch: built_on(),
+      emqx_schema_mod: emqx_schema_mod(edition_type),
+      emqx_machine_boot_apps: emqx_machine_boot_app_list(edition_type),
       is_elixir: "yes"
     ]
   end
@@ -530,6 +579,9 @@ defmodule EMQXUmbrella.MixProject do
     end
   end
 
+  defp emqx_schema_mod(:enterprise), do: :emqx_enterprise_conf_schema
+  defp emqx_schema_mod(:community), do: :emqx_conf_schema
+
   defp bcrypt_dep() do
     if enable_bcrypt?(),
       do: [{:bcrypt, github: "emqx/erlang-bcrypt", tag: "0.6.0", override: true}],

+ 68 - 26
rebar.config.erl

@@ -222,8 +222,10 @@ emqx_description(cloud, ee) -> "EMQ X Enterprise Edition";
 emqx_description(cloud, ce) -> "EMQ X Community Edition";
 emqx_description(edge, ce)  -> "EMQ X Edge Edition".
 
-overlay_vars(RelType, PkgType, _Edition) ->
-    overlay_vars_rel(RelType) ++ overlay_vars_pkg(PkgType).
+overlay_vars(RelType, PkgType, Edition) ->
+    overlay_vars_rel(RelType)
+    ++ overlay_vars_pkg(PkgType)
+    ++ overlay_vars_edition(Edition).
 
 %% vars per release type, cloud or edge
 overlay_vars_rel(RelType) ->
@@ -235,6 +237,15 @@ overlay_vars_rel(RelType) ->
     [ {vm_args_file, VmArgs}
     ].
 
+overlay_vars_edition(ce) ->
+    [ {emqx_schema_mod, emqx_conf_schema}
+    , {emqx_machine_boot_apps, emqx_machine_boot_app_list(ce)}
+    ];
+overlay_vars_edition(ee) ->
+    [ {emqx_schema_mod, emqx_enterprise_conf_schema}
+    , {emqx_machine_boot_apps, emqx_machine_boot_app_list(ee)}
+    ].
+
 %% vars per packaging type, bin(zip/tar.gz/docker) or pkg(rpm/deb)
 overlay_vars_pkg(bin) ->
     [ {platform_bin_dir, "bin"}
@@ -316,10 +327,7 @@ relx_apps(ReleaseType, Edition) ->
     %++ [emqx_license || is_enterprise(Edition)]
     ++ [bcrypt || provide_bcrypt_release(ReleaseType)]
     ++ relx_apps_per_rel(ReleaseType)
-       %% NOTE: applications below are only loaded after node start/restart
-       %% TODO: Add loaded/unloaded state to plugin apps
-       %%       then we can always start plugin apps
-    ++ [{N, load} || N <- relx_plugin_apps(ReleaseType, Edition)].
+    ++ relx_additional_apps(ReleaseType, Edition).
 
 relx_apps_per_rel(cloud) ->
     [ xmerl
@@ -335,19 +343,49 @@ is_app(Name) ->
         _ -> false
     end.
 
-relx_plugin_apps(ReleaseType, Edition) ->
+relx_additional_apps(ReleaseType, Edition) ->
     relx_plugin_apps_per_rel(ReleaseType)
-    ++ relx_plugin_apps_enterprise(Edition).
+    ++ relx_apps_per_edition(Edition).
 
 relx_plugin_apps_per_rel(cloud) ->
     [];
 relx_plugin_apps_per_rel(edge) ->
     [].
 
-relx_plugin_apps_enterprise(ee) ->
-    [list_to_atom(A) || A <- filelib:wildcard("*", "lib-ee"),
-                        filelib:is_dir(filename:join(["lib-ee", A]))];
-relx_plugin_apps_enterprise(ce) -> [].
+relx_apps_per_edition(ee) ->
+    [ emqx_license
+    , {emqx_enterprise_conf, load}
+    ];
+
+relx_apps_per_edition(ce) -> [].
+
+emqx_machine_boot_apps(ce) ->
+    [ emqx_prometheus
+    , emqx_modules
+    , emqx_dashboard
+    , emqx_connector
+    , emqx_gateway
+    , emqx_statsd
+    , emqx_resource
+    , emqx_rule_engine
+    , emqx_bridge
+    , emqx_plugin_libs
+    , emqx_management
+    , emqx_retainer
+    , emqx_exhook
+    , emqx_authn
+    , emqx_authz
+    , emqx_plugin
+    ];
+
+emqx_machine_boot_apps(ee) ->
+    emqx_machine_boot_apps(ce) ++
+    [].
+
+emqx_machine_boot_app_list(Edition) ->
+    string:join(
+      [atom_to_list(AppName) || AppName <- emqx_machine_boot_apps(Edition)],
+      ", ").
 
 relx_overlay(ReleaseType, Edition) ->
     [ {mkdir, "log/"}
@@ -374,34 +412,38 @@ relx_overlay(ReleaseType, Edition) ->
     , {copy, "bin/nodetool", "bin/nodetool-{{release_version}}"}
     ] ++ etc_overlay(ReleaseType, Edition).
 
-etc_overlay(ReleaseType, _Edition) ->
-    Templates = emqx_etc_overlay(ReleaseType),
+etc_overlay(ReleaseType, Edition) ->
+    Templates = emqx_etc_overlay(ReleaseType, Edition),
     [ {mkdir, "etc/"}
     , {copy, "{{base_dir}}/lib/emqx/etc/certs","etc/"}
     ] ++
     lists:map(
       fun({From, To}) -> {template, From, To};
          (FromTo)     -> {template, FromTo, FromTo}
-      end, Templates)
-    ++ extra_overlay(ReleaseType).
+      end, Templates).
 
-extra_overlay(cloud) ->
-    [
-    ];
-extra_overlay(edge) ->
-    [].
-emqx_etc_overlay(cloud) ->
-    emqx_etc_overlay_common() ++
+emqx_etc_overlay(ReleaseType, Edition) ->
+    emqx_etc_overlay_per_rel(ReleaseType)
+    ++ emqx_etc_overlay_per_edition(Edition)
+    ++ emqx_etc_overlay_common().
+
+emqx_etc_overlay_per_rel(cloud) ->
     [ {"{{base_dir}}/lib/emqx/etc/emqx_cloud/vm.args","etc/vm.args"}
     ];
-emqx_etc_overlay(edge) ->
-    emqx_etc_overlay_common() ++
+emqx_etc_overlay_per_rel(edge) ->
     [ {"{{base_dir}}/lib/emqx/etc/emqx_edge/vm.args","etc/vm.args"}
     ].
 
 emqx_etc_overlay_common() ->
+    [ {"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"}
+    ].
+
+emqx_etc_overlay_per_edition(ce) ->
     [ {"{{base_dir}}/lib/emqx_conf/etc/emqx.conf.all", "etc/emqx.conf"}
-    , {"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"}
+    ];
+emqx_etc_overlay_per_edition(ee) ->
+    [ {"{{base_dir}}/lib/emqx_conf/etc/emqx_enterprise.conf.all", "etc/emqx_enterprise.conf"}
+    , {"{{base_dir}}/lib/emqx_conf/etc/emqx.conf.all", "etc/emqx.conf"}
     ].
 
 get_vsn(Profile) ->

+ 1 - 1
rel/emqx_vars

@@ -14,8 +14,8 @@ RUNNER_ETC_DIR="{{ runner_etc_dir }}"
 RUNNER_DATA_DIR="{{ runner_data_dir }}"
 RUNNER_USER="{{ runner_user }}"
 IS_ELIXIR="{{ is_elixir }}"
+SCHEMA_MOD="{{ emqx_schema_mod }}"
 
-EMQX_LICENSE_CONF=''
 export EMQX_DESCRIPTION='{{ emqx_description }}'
 
 ## computed vars

+ 3 - 0
scripts/check-deps-integrity.escript

@@ -36,6 +36,9 @@ collect_deps([File | Files], Acc) ->
     collect_deps(Files, do_collect_deps(Deps, File, Acc)).
 
 do_collect_deps([], _File, Acc) -> Acc;
+%% ignore relative app dependencies
+do_collect_deps([{_Name, {path, _Path}} | Deps], File, Acc) ->
+    do_collect_deps(Deps, File, Acc);
 do_collect_deps([{Name, Ref} | Deps], File, Acc) ->
     Refs = maps:get(Name, Acc, []),
     do_collect_deps(Deps, File, Acc#{Name => [{Ref, File} | Refs]}).

+ 3 - 3
scripts/check-elixir-applications.exs

@@ -22,7 +22,7 @@ defmodule CheckElixirApplications do
       env: [{"DEBUG", "1"}]
     )
 
-    mix_apps = mix_applications(inputs.release_type)
+    mix_apps = mix_applications(inputs.release_type, inputs.edition_type)
     rebar_apps = rebar_applications(profile)
     results = diff_apps(mix_apps, rebar_apps)
 
@@ -70,8 +70,8 @@ defmodule CheckElixirApplications do
     end
   end
 
-  defp mix_applications(release_type) do
-    EMQXUmbrella.MixProject.applications(release_type)
+  defp mix_applications(release_type, edition_type) do
+    EMQXUmbrella.MixProject.applications(release_type, edition_type)
   end
 
   defp rebar_applications(profile) do

+ 81 - 0
scripts/check-elixir-emqx-machine-boot-discrepancies.exs

@@ -0,0 +1,81 @@
+#!/usr/bin/env elixir
+
+defmodule CheckElixirEMQXMachineBootDiscrepancies do
+  alias EMQXUmbrella.MixProject
+
+  def main() do
+    {:ok, _} = Application.ensure_all_started(:mix)
+
+    File.cwd!()
+    |> Path.join("mix.exs")
+    |> Code.compile_file()
+
+    inputs = MixProject.check_profile!()
+    profile = Mix.env()
+    # produce `rebar.config.rendered` to consult
+
+    File.cwd!()
+    |> Path.join("rebar3")
+    |> System.cmd(["as", to_string(profile)],
+      env: [{"DEBUG", "1"}]
+    )
+
+    mix_apps = mix_emqx_machine_applications(inputs.edition_type)
+    rebar_apps = rebar_emqx_machine_applications(profile)
+    {mix_missing, rebar_missing} = diff_apps(mix_apps, rebar_apps)
+
+    if Enum.any?(mix_missing) do
+      IO.puts(
+        "For profile=#{profile}, edition=#{inputs.edition_type} " <>
+        "rebar.config.erl has the following emqx_machine_boot_apps " <>
+        "that are missing in mix.exs:"
+      )
+      IO.inspect(mix_missing, syntax_colors: [atom: :red])
+    end
+
+    if Enum.any?(rebar_missing) do
+      IO.puts(
+        "For profile=#{profile}, edition=#{inputs.edition_type} " <>
+        "mix.exs has the following emqx_machine_boot_apps " <>
+        "that are missing in rebar3.config.erl:"
+      )
+      IO.inspect(rebar_missing, syntax_colors: [atom: :red])
+    end
+
+    success? = Enum.empty?(mix_missing) and Enum.empty?(rebar_missing)
+
+    if not success? do
+      System.halt(1)
+    else
+      IO.puts(
+        IO.ANSI.green() <>
+          "Mix and Rebar emqx_machine_boot_apps OK!" <>
+          IO.ANSI.reset()
+      )
+    end
+  end
+
+  defp mix_emqx_machine_applications(edition_type) do
+    EMQXUmbrella.MixProject.emqx_machine_boot_apps(edition_type)
+  end
+
+  defp rebar_emqx_machine_applications(profile) do
+    {:ok, props} =
+      File.cwd!()
+      |> Path.join("rebar.config.rendered")
+      |> :file.consult()
+
+    props[:profiles][profile][:relx][:overlay_vars][:emqx_machine_boot_apps]
+    |> to_string()
+    |> String.split(~r/,\s+/)
+    |> Enum.map(&String.to_atom/1)
+  end
+
+  defp diff_apps(mix_apps, rebar_apps) do
+    mix_missing = rebar_apps -- mix_apps
+    rebar_missing = mix_apps -- rebar_apps
+    {mix_missing, rebar_missing}
+  end
+end
+
+CheckElixirEMQXMachineBootDiscrepancies.main()

+ 22 - 9
scripts/merge-config.escript

@@ -12,16 +12,29 @@
 
 main(_) ->
     {ok, BaseConf} = file:read_file("apps/emqx_conf/etc/emqx_conf.conf"),
+
     Cfgs = get_all_cfgs("apps/"),
-    Conf = lists:foldl(fun(CfgFile, Acc) ->
-                               case filelib:is_regular(CfgFile) of
-                                   true ->
-                                       {ok, Bin1} = file:read_file(CfgFile),
-                                       [Acc, io_lib:nl(), Bin1];
-                                   false -> Acc
-                               end
-                       end, BaseConf, Cfgs),
-    ok = file:write_file("apps/emqx_conf/etc/emqx.conf.all", Conf).
+    Conf = [merge(BaseConf, Cfgs),
+            io_lib:nl(),
+            "include emqx_enterprise.conf",
+            io_lib:nl()],
+    ok = file:write_file("apps/emqx_conf/etc/emqx.conf.all", Conf),
+
+    EnterpriseCfgs = get_all_cfgs("lib-ee/"),
+    EnterpriseConf = merge("", EnterpriseCfgs),
+
+    ok = file:write_file("apps/emqx_conf/etc/emqx_enterprise.conf.all", EnterpriseConf).
+
+merge(BaseConf, Cfgs) ->
+    lists:foldl(
+      fun(CfgFile, Acc) ->
+              case filelib:is_regular(CfgFile) of
+                  true ->
+                      {ok, Bin1} = file:read_file(CfgFile),
+                      [Acc, io_lib:nl(), Bin1];
+                  false -> Acc
+              end
+      end, BaseConf, Cfgs).
 
 get_all_cfgs(Root) ->
     Apps = filelib:wildcard("*", Root) -- ["emqx_machine", "emqx_conf"],