feat(license): add HTTP API for license
This commit is contained in:
parent
da6d6e8a9d
commit
b19e8fb3cd
|
@ -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: "更新许可证"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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">>}.
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
|
@ -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"),
|
||||
|
|
|
@ -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) ->
|
||||
|
|
Loading…
Reference in New Issue