feat: check all config schema before loading

This commit is contained in:
zhongwencool 2023-06-09 15:28:28 +08:00
parent b829d8edc2
commit cf9a207743
6 changed files with 122 additions and 14 deletions

View File

@ -44,6 +44,8 @@
code_change/3 code_change/3
]). ]).
-export([schema/2]).
-define(MOD, {mod}). -define(MOD, {mod}).
-define(WKEY, '?'). -define(WKEY, '?').

View File

@ -161,4 +161,4 @@ authn_list(Authn) when is_map(Authn) ->
[Authn]. [Authn].
merge_config(AuthNs) -> merge_config(AuthNs) ->
emqx_authn_api:update_config(?CONF_NS_BINARY, {merge_authenticators, AuthNs}). emqx_authn_api:update_config([?CONF_NS_ATOM], {merge_authenticators, AuthNs}).

View File

@ -89,6 +89,8 @@
param_listener_id/0 param_listener_id/0
]). ]).
-export([update_config/2]).
-elvis([{elvis_style, god_modules, disable}]). -elvis([{elvis_style, god_modules, disable}]).
api_spec() -> api_spec() ->

View File

@ -32,6 +32,7 @@
-export([schema_module/0]). -export([schema_module/0]).
-export([gen_example_conf/2]). -export([gen_example_conf/2]).
-export([reload_etc_conf_on_local_node/0]). -export([reload_etc_conf_on_local_node/0]).
-export([check_config/2]).
%% TODO: move to emqx_dashboard when we stop building api schema at build time %% TODO: move to emqx_dashboard when we stop building api schema at build time
-export([ -export([
@ -280,19 +281,38 @@ load_etc_config_file() ->
check_readonly_config(Raw) -> check_readonly_config(Raw) ->
SchemaMod = schema_module(), SchemaMod = schema_module(),
RawDefault = emqx_config:fill_defaults(Raw), RawDefault = emqx_config:fill_defaults(Raw),
{_AppEnvs, CheckedConf} = emqx_config:check_config(SchemaMod, RawDefault), case check_config(SchemaMod, RawDefault) of
case lists:filtermap(fun(Key) -> filter_changed(Key, CheckedConf) end, ?READONLY_KEYS) of {ok, CheckedConf} ->
[] -> case
{ok, maps:without([atom_to_binary(K) || K <- ?READONLY_KEYS], Raw)}; lists:filtermap(fun(Key) -> filter_changed(Key, CheckedConf) end, ?READONLY_KEYS)
Error -> of
[] ->
{ok, maps:without([atom_to_binary(K) || K <- ?READONLY_KEYS], Raw)};
Error ->
?SLOG(error, #{
msg => "failed_to_change_read_only_key_in_etc_config",
read_only_keys => ?READONLY_KEYS,
error => Error
}),
{error, Error}
end;
{error, Error} ->
?SLOG(error, #{ ?SLOG(error, #{
msg => "failed_to_change_read_only_key_in_etc_config", msg => "failed_to_check_etc_config",
read_only_keys => ?READONLY_KEYS,
error => Error error => Error
}), }),
{error, Error} {error, Error}
end. end.
check_config(Mod, Raw) ->
try
{_AppEnvs, CheckedConf} = emqx_config:check_config(Mod, Raw),
{ok, CheckedConf}
catch
throw:Error ->
{error, Error}
end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -188,14 +188,17 @@ load_config(Path, AuthChain) ->
emqx_ctl:warning("load ~ts is empty~n", [Path]), emqx_ctl:warning("load ~ts is empty~n", [Path]),
{error, empty_hocon_file}; {error, empty_hocon_file};
{ok, RawConf} -> {ok, RawConf} ->
case check_config_keys(RawConf) of case check_config(RawConf) of
ok -> ok ->
maps:foreach(fun(K, V) -> update_config(K, V, AuthChain) end, RawConf); maps:foreach(fun(K, V) -> update_config(K, V, AuthChain) end, RawConf);
{error, Reason} -> {error, Reason} when is_list(Reason) ->
emqx_ctl:warning("load ~ts failed~n~ts~n", [Path, Reason]), emqx_ctl:warning("load ~ts failed~n~ts~n", [Path, Reason]),
emqx_ctl:warning( emqx_ctl:warning(
"Maybe try `emqx_ctl conf reload` to reload etc/emqx.conf on local node~n" "Maybe try `emqx_ctl conf reload` to reload etc/emqx.conf on local node~n"
), ),
{error, Reason};
{error, Reason} when is_map(Reason) ->
emqx_ctl:warning("load ~ts schema check failed~n~p~n", [Path, Reason]),
{error, Reason} {error, Reason}
end; end;
{error, Reason} -> {error, Reason} ->
@ -216,10 +219,35 @@ update_config(Key, Value, _) ->
check_res(Key, {ok, _}) -> emqx_ctl:print("load ~ts in cluster ok~n", [Key]); check_res(Key, {ok, _}) -> emqx_ctl:print("load ~ts in cluster ok~n", [Key]);
check_res(Key, {error, Reason}) -> emqx_ctl:warning("load ~ts failed~n~p~n", [Key, Reason]). check_res(Key, {error, Reason}) -> emqx_ctl:warning("load ~ts failed~n~p~n", [Key, Reason]).
check_config_keys(Conf) -> check_config(Conf) ->
case check_keys_is_not_readonly(Conf) of
ok -> check_config_schema(Conf);
Error -> Error
end.
check_keys_is_not_readonly(Conf) ->
Keys = maps:keys(Conf), Keys = maps:keys(Conf),
ReadOnlyKeys = [atom_to_binary(K) || K <- ?READONLY_KEYS], ReadOnlyKeys = [atom_to_binary(K) || K <- ?READONLY_KEYS],
case ReadOnlyKeys -- Keys of case ReadOnlyKeys -- Keys of
ReadOnlyKeys -> ok; ReadOnlyKeys -> ok;
_ -> {error, "update_readonly_keys_prohibited"} _ -> {error, "update_readonly_keys_prohibited"}
end. end.
check_config_schema(Conf) ->
SchemaMod = emqx_conf:schema_module(),
Res =
maps:fold(
fun(Key, Value, Acc) ->
Schema = emqx_config_handler:schema(SchemaMod, [Key]),
case emqx_conf:check_config(Schema, #{Key => Value}) of
{ok, _} -> Acc;
{error, Reason} -> #{Key => Reason}
end
end,
#{},
Conf
),
case Res =:= #{} of
true -> ok;
false -> {error, Res}
end.

View File

@ -56,17 +56,73 @@ t_load_config(Config) ->
ok. ok.
t_load_readonly(Config) -> t_load_readonly(Config) ->
Base = #{<<"mqtt">> => emqx_conf:get_raw([mqtt])}, Base0 = base_conf(),
Base1 = Base0#{<<"mqtt">> => emqx_conf:get_raw([mqtt])},
lists:foreach( lists:foreach(
fun(Key) -> fun(Key) ->
KeyBin = atom_to_binary(Key),
Conf = emqx_conf:get_raw([Key]), Conf = emqx_conf:get_raw([Key]),
ConfBin0 = hocon_pp:do(Base#{Key => Conf}, #{}), ConfBin0 = hocon_pp:do(Base1#{KeyBin => Conf}, #{}),
ConfFile0 = prepare_conf_file(?FUNCTION_NAME, ConfBin0, Config), ConfFile0 = prepare_conf_file(?FUNCTION_NAME, ConfBin0, Config),
?assertEqual( ?assertEqual(
{error, "update_readonly_keys_prohibited"}, {error, "update_readonly_keys_prohibited"},
emqx_conf_cli:conf(["load", ConfFile0]) emqx_conf_cli:conf(["load", ConfFile0])
) ),
%% reload etc/emqx.conf changed readonly keys
ConfBin1 = hocon_pp:do(Base1#{KeyBin => changed(Key)}, #{}),
ConfFile1 = prepare_conf_file(?FUNCTION_NAME, ConfBin1, Config),
application:set_env(emqx, config_files, [ConfFile1]),
?assertMatch({error, [{Key, #{changed := _}}]}, emqx_conf_cli:conf(["reload"]))
end, end,
?READONLY_KEYS ?READONLY_KEYS
), ),
ok. ok.
t_error_schema_check(Config) ->
Base = #{
%% bad multiplier
<<"mqtt">> => #{<<"keepalive_multiplier">> => -1},
<<"zones">> => #{<<"my-zone">> => #{<<"mqtt">> => #{<<"keepalive_multiplier">> => 10}}}
},
ConfBin0 = hocon_pp:do(Base, #{}),
ConfFile0 = prepare_conf_file(?FUNCTION_NAME, ConfBin0, Config),
?assertMatch({error, _}, emqx_conf_cli:conf(["load", ConfFile0])),
%% zones is not updated because of error
?assertEqual(#{}, emqx_config:get_raw([zones])),
ok.
t_reload_etc_emqx_conf_not_persistent(Config) ->
Mqtt = emqx_conf:get_raw([mqtt]),
Base = base_conf(),
Conf = Base#{<<"mqtt">> => Mqtt#{<<"keepalive_multiplier">> => 3}},
ConfBin = hocon_pp:do(Conf, #{}),
ConfFile = prepare_conf_file(?FUNCTION_NAME, ConfBin, Config),
application:set_env(emqx, config_files, [ConfFile]),
ok = emqx_conf_cli:conf(["reload"]),
?assertEqual(3, emqx:get_config([mqtt, keepalive_multiplier])),
?assertNotEqual(
3,
emqx_utils_maps:deep_get(
[<<"mqtt">>, <<"keepalive_multiplier">>],
emqx_config:read_override_conf(#{}),
undefined
)
),
ok.
base_conf() ->
#{
<<"cluster">> => emqx_conf:get_raw([cluster]),
<<"node">> => emqx_conf:get_raw([node])
}.
changed(cluster) ->
#{<<"name">> => <<"emqx-test">>};
changed(node) ->
#{
<<"name">> => <<"emqx-test@127.0.0.1">>,
<<"cookie">> => <<"gokdfkdkf1122">>,
<<"data_dir">> => <<"data">>
};
changed(rpc) ->
#{<<"mode">> => <<"sync">>}.