emqx/apps/emqx_management/src/emqx_mgmt_api_configs.erl

238 lines
9.0 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_configs).
-include_lib("hocon/include/hoconsc.hrl").
-behaviour(minirest_api).
-export([api_spec/0]).
-export([paths/0, schema/1, fields/1]).
-export([config/3, config_reset/3, configs/3, get_full_config/0]).
-export([get_conf_schema/2, gen_schema/1]).
-define(PREFIX, "/configs/").
-define(PREFIX_RESET, "/configs_reset/").
-define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))).
-define(EXCLUDES, [listeners, node, cluster, gateway, rule_engine]).
api_spec() ->
emqx_dashboard_swagger:spec(?MODULE).
paths() ->
["/configs", "/configs_reset/:rootname"] ++
lists:map(fun({Name, _Type}) -> ?PREFIX ++ to_list(Name) end, config_list(?EXCLUDES)).
schema("/configs") ->
#{
'operationId' => configs,
get => #{
tags => [conf],
description =>
<<"Get all the configurations of the specified node, including hot and non-hot updatable items.">>,
parameters => [
{node, hoconsc:mk(typerefl:atom(),
#{in => query, required => false, example => <<"emqx@127.0.0.1">>,
desc =>
<<"Node's name: If you do not fill in the fields, this node will be used by default.">>})}],
responses => #{
200 => config_list([])
}
}
};
schema("/configs_reset/:rootname") ->
Paths = lists:map(fun({Path, _}) -> Path end, config_list(?EXCLUDES)),
#{
'operationId' => config_reset,
post => #{
tags => [conf],
description =>
<<"Reset the config entry specified by the query string parameter `conf_path`.<br/>
- For a config entry that has default value, this resets it to the default value;
- For a config entry that has no default value, an error 400 will be returned">>,
%% We only return "200" rather than the new configs that has been changed, as
%% the schema of the changed configs is depends on the request parameter
%% `conf_path`, it cannot be defined here.
parameters => [
{rootname, hoconsc:mk( hoconsc:enum(Paths)
, #{in => path, example => <<"authorization">>})},
{conf_path, hoconsc:mk(typerefl:binary(),
#{in => query, required => false, example => <<"cache.enable">>,
desc => <<"The config path separated by '.' character">>})}],
responses => #{
200 => <<"Rest config successfully">>,
400 => emqx_dashboard_swagger:error_codes(['NO_DEFAULT_VALUE', 'REST_FAILED'])
}
}
};
schema(Path) ->
{Root, Schema} = find_schema(Path),
#{
'operationId' => config,
get => #{
tags => [conf],
description => iolist_to_binary([ <<"Get the sub-configurations under *">>
, Root
, <<"*">>]),
responses => #{
200 => Schema,
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"config not found">>)
}
},
put => #{
tags => [conf],
description => iolist_to_binary([ <<"Update the sub-configurations under *">>
, Root
, <<"*">>]),
'requestBody' => Schema,
responses => #{
200 => Schema,
400 => emqx_dashboard_swagger:error_codes(['UPDATE_FAILED'])
}
}
}.
find_schema(Path) ->
[_, _Prefix, Root | _] = string:split(Path, "/", all),
Configs = config_list(?EXCLUDES),
case lists:keyfind(Root, 1, Configs) of
{Root, Schema} -> {Root, Schema};
false ->
RootAtom = list_to_existing_atom(Root),
{Root, element(2, lists:keyfind(RootAtom, 1, Configs))}
end.
%% we load all configs from emqx_conf_schema, some of them are defined as local ref
%% we need redirect to emqx_conf_schema.
%% such as hoconsc:ref("node") to hoconsc:ref(emqx_conf_schema, "node")
fields(Field) -> emqx_conf_schema:fields(Field).
%%%==============================================================================================
%% HTTP API Callbacks
config(get, _Params, Req) ->
Path = conf_path(Req),
case emqx_map_lib:deep_find(Path, get_full_config()) of
{ok, Conf} ->
{200, Conf};
{not_found, _, _} ->
{404, #{code => 'NOT_FOUND', message => <<"Config cannot found">>}}
end;
config(put, #{body := Body}, Req) ->
Path = conf_path(Req),
case emqx:update_config(Path, Body, #{rawconf_with_defaults => true}) of
{ok, #{raw_config := RawConf}} ->
{200, emqx_map_lib:jsonable_map(RawConf)};
{error, Reason} ->
{400, #{code => 'UPDATE_FAILED', message => ?ERR_MSG(Reason)}}
end.
config_reset(post, _Params, Req) ->
%% reset the config specified by the query string param 'conf_path'
Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req),
case emqx:reset_config(Path, #{}) of
{ok, _} -> {200};
{error, no_default_value} ->
{400, #{code => 'NO_DEFAULT_VALUE', message => <<"No Default Value.">>}};
{error, Reason} ->
{400, #{code => 'REST_FAILED', message => ?ERR_MSG(Reason)}}
end.
configs(get, Params, _Req) ->
Node = maps:get(node, Params, node()),
case
lists:member(Node, mria_mnesia:running_nodes())
andalso
rpc:call(Node, ?MODULE, get_full_config, [])
of
false ->
Message = list_to_binary(io_lib:format("Bad node ~p, reason not found", [Node])),
{500, #{code => 'BAD_NODE', message => Message}};
{error, {badrpc, R}} ->
Message = list_to_binary(io_lib:format("Bad node ~p, reason ~p", [Node, R])),
{500, #{code => 'BAD_NODE', message => Message}};
Res ->
{200, Res}
end.
conf_path_reset(Req) ->
<<"/api/v5", ?PREFIX_RESET, Path/binary>> = cowboy_req:path(Req),
string:lexemes(Path, "/ ").
get_full_config() ->
emqx_map_lib:jsonable_map(
emqx_config:fill_defaults(emqx:get_raw_config([]))).
conf_path_from_querystr(Req) ->
case proplists:get_value(<<"conf_path">>, cowboy_req:parse_qs(Req)) of
undefined -> [];
Path -> string:lexemes(Path, ". ")
end.
config_list(Exclude) ->
Roots = emqx_conf_schema:roots(),
lists:foldl(fun(Key, Acc) -> lists:delete(Key, Acc) end, Roots, Exclude).
to_list(L) when is_list(L) -> L;
to_list(Atom) when is_atom(Atom) -> atom_to_list(Atom).
conf_path(Req) ->
<<"/api/v5", ?PREFIX, Path/binary>> = cowboy_req:path(Req),
string:lexemes(Path, "/ ").
get_conf_schema(Conf, MaxDepth) ->
get_conf_schema([], maps:to_list(Conf), [], MaxDepth).
get_conf_schema(_BasePath, [], Result, _MaxDepth) ->
Result;
get_conf_schema(BasePath, [{Key, Conf} | Confs], Result, MaxDepth) ->
Path = BasePath ++ [Key],
Depth = length(Path),
Result1 = case is_map(Conf) of
true when Depth < MaxDepth ->
get_conf_schema(Path, maps:to_list(Conf), Result, MaxDepth);
true when Depth >= MaxDepth -> Result;
false -> Result
end,
get_conf_schema(BasePath, Confs, [{Path, gen_schema(Conf)} | Result1], MaxDepth).
%% TODO: generate from hocon schema
gen_schema(Conf) when is_boolean(Conf) ->
with_default_value(#{type => boolean}, Conf);
gen_schema(Conf) when is_binary(Conf); is_atom(Conf) ->
with_default_value(#{type => string}, Conf);
gen_schema(Conf) when is_number(Conf) ->
with_default_value(#{type => number}, Conf);
gen_schema(Conf) when is_list(Conf) ->
case io_lib:printable_unicode_list(Conf) of
true ->
gen_schema(unicode:characters_to_binary(Conf));
false ->
#{type => array, items => gen_schema(hd(Conf))}
end;
gen_schema(Conf) when is_map(Conf) ->
#{type => object, properties =>
maps:map(fun(_K, V) -> gen_schema(V) end, Conf)};
gen_schema(_Conf) ->
%% the conf is not of JSON supported type, it may have been converted
%% by the hocon schema
#{type => string}.
with_default_value(Type, Value) ->
Type#{example => emqx_map_lib:binary_string(Value)}.