Sfoglia il codice sorgente

feat(license): add HTTP API for license

Thales Macedo Garitezi 3 anni fa
parent
commit
b19e8fb3cd

+ 23 - 0
lib-ee/emqx_license/i18n/emqx_license_http_api.conf

@@ -0,0 +1,23 @@
+emqx_license_http_api {
+  desc_license_info_api {
+    desc {
+      en: "Get license info"
+      zh: "获取许可证信息"
+    }
+    label: {
+      en: "License info"
+      zh: "许可证信息"
+    }
+  }
+
+  desc_license_upload_api {
+    desc {
+      en: "Upload a license file or key"
+      zh: "上传许可证文件或钥匙"
+    }
+    label: {
+      en: "Update license"
+      zh: "更新许可证"
+    }
+  }
+}

+ 142 - 0
lib-ee/emqx_license/src/emqx_license_http_api.erl

@@ -0,0 +1,142 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_http_api).
+
+-behaviour(minirest_api).
+
+-include_lib("hocon/include/hoconsc.hrl").
+-include_lib("emqx/include/logger.hrl").
+
+-export([
+    namespace/0,
+    api_spec/0,
+    paths/0,
+    schema/1
+]).
+
+-export([
+    '/license'/2,
+    '/license/upload'/2
+]).
+
+-define(BAD_REQUEST, 'BAD_REQUEST').
+-define(NOT_FOUND, 'NOT_FOUND').
+
+namespace() -> "license_http_api".
+
+api_spec() ->
+    emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}).
+
+paths() ->
+    [
+        "/license",
+        "/license/upload"
+    ].
+
+schema("/license") ->
+    #{
+        'operationId' => '/license',
+        get => #{
+            tags => [<<"license">>],
+            summary => <<"Get license info">>,
+            description => ?DESC("desc_license_info_api"),
+            responses => #{
+                200 => emqx_dashboard_swagger:schema_with_examples(
+                    map(),
+                    #{
+                        sample_license_info => #{
+                            value => #{
+                                customer => "Foo",
+                                customer_type => 10,
+                                deployment => "bar-deployment",
+                                email => "contact@foo.com",
+                                expiry => false,
+                                expiry_at => "2295-10-27",
+                                max_connections => 10,
+                                start_at => "2022-01-11",
+                                type => "trial"
+                            }
+                        }
+                    }
+                )
+            }
+        }
+    };
+schema("/license/upload") ->
+    #{
+        'operationId' => '/license/upload',
+        post => #{
+            tags => [<<"license">>],
+            summary => <<"Upload license">>,
+            description => ?DESC("desc_license_upload_api"),
+            'requestBody' => emqx_dashboard_swagger:schema_with_examples(
+                emqx_license_schema:license_type(),
+                #{
+                    license_key => #{
+                        summary => <<"License key string">>,
+                        value => #{
+                            <<"key">> => <<"xxx">>,
+                            <<"connection_low_watermark">> => "75%",
+                            <<"connection_high_watermark">> => "80%"
+                        }
+                    },
+                    license_file => #{
+                        summary => <<"Path to a license file">>,
+                        value => #{
+                            <<"file">> => <<"/path/to/license">>,
+                            <<"connection_low_watermark">> => "75%",
+                            <<"connection_high_watermark">> => "80%"
+                        }
+                    }
+                }
+            ),
+            responses => #{
+                200 => <<"ok">>,
+                400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"bad request">>),
+                404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"file not found">>)
+            }
+        }
+    }.
+
+'/license'(get, _Params) ->
+    License = maps:from_list(emqx_license_checker:dump()),
+    {200, License}.
+
+'/license/upload'(post, #{body := #{<<"file">> := Filepath}}) ->
+    case emqx_license:update_file(Filepath) of
+        {error, enoent} ->
+            ?SLOG(error, #{
+                msg => "license_file_not_found",
+                path => Filepath
+            }),
+            {404, <<"file not found">>};
+        {error, Error} ->
+            ?SLOG(error, #{
+                msg => "bad_license_file",
+                reason => Error,
+                path => Filepath
+            }),
+            {400, <<"bad request">>};
+        {ok, _} ->
+            ?SLOG(info, #{
+                msg => "updated_license_file",
+                path => Filepath
+            }),
+            {200, <<"ok">>}
+    end;
+'/license/upload'(post, #{body := #{<<"key">> := Key}}) ->
+    case emqx_license:update_key(Key) of
+        {error, Error} ->
+            ?SLOG(error, #{
+                msg => "bad_license_key",
+                reason => Error
+            }),
+            {400, <<"bad request">>};
+        {ok, _} ->
+            ?SLOG(info, #{msg => "updated_license_key"}),
+            {200, <<"ok">>}
+    end;
+'/license/upload'(post, _Params) ->
+    {400, <<"bad request">>}.

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

@@ -142,7 +142,6 @@ setup_test(TestCase, Config) when
                     RawConfig = #{<<"type">> => file, <<"file">> => LicensePath},
                     emqx_config:put_raw([<<"license">>], RawConfig),
                     ok = meck:new(emqx_license, [non_strict, passthrough, no_history, no_link]),
-                    %% meck:expect(emqx_license, read_license, fun() -> {ok, License} end),
                     meck:expect(
                         emqx_license_parser,
                         parse,

+ 210 - 0
lib-ee/emqx_license/test/emqx_license_http_api_SUITE.erl

@@ -0,0 +1,210 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_license_http_api_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").
+
+%%------------------------------------------------------------------------------
+%% CT boilerplate
+%%------------------------------------------------------------------------------
+
+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),
+    ok = meck:new(emqx_license_parser, [non_strict, passthrough, no_history, no_link]),
+    ok = meck:expect(
+        emqx_license_parser,
+        parse,
+        fun(X) ->
+            emqx_license_parser:parse(
+                X,
+                emqx_license_test_lib:public_key_pem()
+            )
+        end
+    ),
+    emqx_common_test_helpers:start_apps([emqx_license, emqx_dashboard], fun set_special_configs/1),
+    Config.
+
+end_per_suite(_) ->
+    emqx_common_test_helpers:stop_apps([emqx_license, emqx_dashboard]),
+    ok = meck:unload([emqx_license_parser]),
+    Config = #{type => file, file => emqx_license_test_lib:default_license()},
+    emqx_config:put([license], Config),
+    RawConfig = #{<<"type">> => file, <<"file">> => emqx_license_test_lib:default_license()},
+    emqx_config:put_raw([<<"license">>], RawConfig),
+    ok.
+
+set_special_configs(emqx_dashboard) ->
+    emqx_dashboard_api_test_helpers:set_default_config(<<"license_admin">>);
+set_special_configs(emqx_license) ->
+    LicenseKey = emqx_license_test_lib:make_license(#{max_connections => "100"}),
+    Config = #{type => key, key => LicenseKey},
+    emqx_config:put([license], Config),
+    RawConfig = #{<<"type">> => key, <<"key">> => LicenseKey},
+    emqx_config:put_raw([<<"license">>], RawConfig);
+set_special_configs(_) ->
+    ok.
+
+init_per_testcase(_TestCase, Config) ->
+    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
+    Config.
+
+end_per_testcase(_TestCase, _Config) ->
+    {ok, _} = reset_license(),
+    ok.
+
+%%------------------------------------------------------------------------------
+%% Helper fns
+%%------------------------------------------------------------------------------
+
+request(Method, Uri, Body) ->
+    emqx_dashboard_api_test_helpers:request(<<"license_admin">>, Method, Uri, Body).
+
+uri(Segments) ->
+    emqx_dashboard_api_test_helpers:uri(Segments).
+
+get_license() ->
+    maps:from_list(emqx_license_checker:dump()).
+
+default_license() ->
+    emqx_license_test_lib:make_license(#{max_connections => "100"}).
+
+reset_license() ->
+    emqx_license:update_key(default_license()).
+
+assert_untouched_license() ->
+    ?assertMatch(
+        #{max_connections := 100},
+        get_license()
+    ).
+
+%%------------------------------------------------------------------------------
+%% Testcases
+%%------------------------------------------------------------------------------
+
+t_license_info(_Config) ->
+    Res = request(get, uri(["license"]), []),
+    ?assertMatch({ok, 200, _}, Res),
+    {ok, 200, Payload} = Res,
+    ?assertEqual(
+        #{
+            <<"customer">> => <<"Foo">>,
+            <<"customer_type">> => 10,
+            <<"deployment">> => <<"bar-deployment">>,
+            <<"email">> => <<"contact@foo.com">>,
+            <<"expiry">> => false,
+            <<"expiry_at">> => <<"2295-10-27">>,
+            <<"max_connections">> => 100,
+            <<"start_at">> => <<"2022-01-11">>,
+            <<"type">> => <<"trial">>
+        },
+        emqx_json:decode(Payload, [return_maps])
+    ),
+    ok.
+
+t_license_upload_file_success(_Config) ->
+    NewKey = emqx_license_test_lib:make_license(#{max_connections => "999"}),
+    Path = "/tmp/new.lic",
+    ok = file:write_file(Path, NewKey),
+    try
+        ?assertEqual(
+            {ok, 200, <<"ok">>},
+            request(
+                post,
+                uri(["license", "upload"]),
+                #{file => Path}
+            )
+        ),
+        ?assertMatch(
+            #{max_connections := 999},
+            get_license()
+        ),
+        ok
+    after
+        ok = file:delete(Path),
+        ok
+    end.
+
+t_license_upload_file_not_found(_Config) ->
+    ?assertEqual(
+        {ok, 404, <<"file not found">>},
+        request(
+            post,
+            uri(["license", "upload"]),
+            #{file => "/tmp/inexistent.lic"}
+        )
+    ),
+    assert_untouched_license(),
+    ok.
+
+t_license_upload_file_reading_error(_Config) ->
+    %% eisdir
+    Path = "/tmp/",
+    ?assertEqual(
+        {ok, 400, <<"bad request">>},
+        request(
+            post,
+            uri(["license", "upload"]),
+            #{file => Path}
+        )
+    ),
+    assert_untouched_license(),
+    ok.
+
+t_license_upload_file_bad_license(_Config) ->
+    Path = "/tmp/bad.lic",
+    ok = file:write_file(Path, <<"bad key">>),
+    try
+        ?assertEqual(
+            {ok, 400, <<"bad request">>},
+            request(
+                post,
+                uri(["license", "upload"]),
+                #{file => Path}
+            )
+        ),
+        assert_untouched_license(),
+        ok
+    after
+        ok = file:delete(Path),
+        ok
+    end.
+
+t_license_upload_key_success(_Config) ->
+    NewKey = emqx_license_test_lib:make_license(#{max_connections => "999"}),
+    ?assertEqual(
+        {ok, 200, <<"ok">>},
+        request(
+            post,
+            uri(["license", "upload"]),
+            #{key => NewKey}
+        )
+    ),
+    ?assertMatch(
+        #{max_connections := 999},
+        get_license()
+    ),
+    ok.
+
+t_license_upload_key_bad_key(_Config) ->
+    BadKey = <<"bad key">>,
+    ?assertEqual(
+        {ok, 400, <<"bad request">>},
+        request(
+            post,
+            uri(["license", "upload"]),
+            #{key => BadKey}
+        )
+    ),
+    assert_untouched_license(),
+    ok.

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

@@ -47,6 +47,32 @@ test_key(Filename, Format) ->
             public_key:pem_entry_decode(PemEntry)
     end.
 
+make_license(Values0 = #{}) ->
+    Defaults = #{
+        license_format => "220111",
+        license_type => "0",
+        customer_type => "10",
+        name => "Foo",
+        email => "contact@foo.com",
+        deployment => "bar-deployment",
+        start_date => "20220111",
+        days => "100000",
+        max_connections => "10"
+    },
+    Values1 = maps:merge(Defaults, Values0),
+    Keys = [
+        license_format,
+        license_type,
+        customer_type,
+        name,
+        email,
+        deployment,
+        start_date,
+        days,
+        max_connections
+    ],
+    Values = lists:map(fun(K) -> maps:get(K, Values1) end, Keys),
+    make_license(Values);
 make_license(Values) ->
     Key = private_key(),
     Text = string:join(Values, "\n"),

+ 5 - 3
scripts/merge-i18n.escript

@@ -4,10 +4,12 @@
 
 main(_) ->
     BaseConf = <<"">>,
-    Cfgs = get_all_cfgs("apps/"),
-    Conf = [merge(BaseConf, Cfgs),
+    Cfgs0 = get_all_cfgs("apps/"),
+    Cfgs1 = get_all_cfgs("lib-ee/"),
+    Conf0 = merge(BaseConf, Cfgs0),
+    Conf = [merge(Conf0, Cfgs1),
             io_lib:nl()
-            ],
+           ],
     ok = file:write_file("apps/emqx_dashboard/priv/i18n.conf", Conf).
 
 merge(BaseConf, Cfgs) ->