feat(license): add HTTP API for license

This commit is contained in:
Thales Macedo Garitezi 2022-07-29 13:42:24 -03:00
parent da6d6e8a9d
commit b19e8fb3cd
6 changed files with 406 additions and 4 deletions

View File

@ -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: "更新许可证"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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