Merge pull request #11523 from zhongwencool/emqx-conf-suggest-message

fix: improve the suggest msg for update conf failed
This commit is contained in:
zhongwencool 2023-08-28 14:44:12 +08:00 committed by GitHub
commit d7f6d02270
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 163 additions and 53 deletions

View File

@ -50,17 +50,17 @@ conf(["show"]) ->
conf(["show", Key]) ->
print_hocon(get_config(list_to_binary(Key)));
conf(["load", "--replace", Path]) ->
load_config(Path, replace);
load_config(Path, #{mode => replace});
conf(["load", "--merge", Path]) ->
load_config(Path, merge);
load_config(Path, #{mode => merge});
conf(["load", Path]) ->
load_config(Path, merge);
load_config(Path, #{mode => merge});
conf(["cluster_sync" | Args]) ->
admins(Args);
conf(["reload", "--merge"]) ->
reload_etc_conf_on_local_node(merge);
reload_etc_conf_on_local_node(#{mode => merge});
conf(["reload", "--replace"]) ->
reload_etc_conf_on_local_node(replace);
reload_etc_conf_on_local_node(#{mode => replace});
conf(["reload"]) ->
conf(["reload", "--merge"]);
conf(_) ->
@ -191,32 +191,32 @@ get_config(Key) ->
end.
-define(OPTIONS, #{rawconf_with_defaults => true, override_to => cluster}).
load_config(Path, ReplaceOrMerge) when is_list(Path) ->
load_config(Path, Opts) when is_list(Path) ->
case hocon:files([Path]) of
{ok, RawConf} when RawConf =:= #{} ->
emqx_ctl:warning("load ~ts is empty~n", [Path]),
{error, empty_hocon_file};
{ok, RawConf} ->
load_config_from_raw(RawConf, ReplaceOrMerge);
load_config_from_raw(RawConf, Opts);
{error, Reason} ->
emqx_ctl:warning("load ~ts failed~n~p~n", [Path, Reason]),
{error, bad_hocon_file}
end;
load_config(Bin, ReplaceOrMerge) when is_binary(Bin) ->
load_config(Bin, Opts) when is_binary(Bin) ->
case hocon:binary(Bin) of
{ok, RawConf} ->
load_config_from_raw(RawConf, ReplaceOrMerge);
load_config_from_raw(RawConf, Opts);
{error, Reason} ->
{error, Reason}
end.
load_config_from_raw(RawConf, ReplaceOrMerge) ->
load_config_from_raw(RawConf, Opts) ->
case check_config(RawConf) of
ok ->
Error =
lists:filtermap(
fun({K, V}) ->
case update_config_cluster(K, V, ReplaceOrMerge) of
case update_config_cluster(K, V, Opts) of
ok -> false;
{error, Msg} -> {true, Msg}
end
@ -228,53 +228,70 @@ load_config_from_raw(RawConf, ReplaceOrMerge) ->
ErrorBin -> {error, ErrorBin}
end;
{error, ?UPDATE_READONLY_KEYS_PROHIBITED = Reason} ->
emqx_ctl:warning("load config failed~n~ts~n", [Reason]),
emqx_ctl:warning(
"Maybe try `emqx_ctl conf reload` to reload etc/emqx.conf on local node~n"
warning(Opts, "load config failed~n~ts~n", [Reason]),
warning(
Opts,
"Maybe try `emqx_ctl conf reload` to reload etc/emqx.conf on local node~n",
[]
),
{error, Reason};
{error, Errors} ->
emqx_ctl:warning("load schema check failed~n"),
warning(Opts, "load schema check failed~n", []),
lists:foreach(
fun({Key, Error}) ->
emqx_ctl:warning("~ts: ~p~n", [Key, Error])
warning(Opts, "~ts: ~p~n", [Key, Error])
end,
Errors
),
{error, Errors}
end.
update_config_cluster(?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_BINARY = Key, Conf, merge = Mode) ->
check_res(Key, emqx_authz:merge(Conf), Conf, Mode);
update_config_cluster(?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY = Key, Conf, merge = Mode) ->
check_res(Key, emqx_authn:merge_config(Conf), Conf, Mode);
update_config_cluster(Key, NewConf, merge = Mode) ->
update_config_cluster(
?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_BINARY = Key,
Conf,
#{mode := merge} = Opts
) ->
check_res(Key, emqx_authz:merge(Conf), Conf, Opts);
update_config_cluster(
?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY = Key,
Conf,
#{mode := merge} = Opts
) ->
check_res(Key, emqx_authn:merge_config(Conf), Conf, Opts);
update_config_cluster(Key, NewConf, #{mode := merge} = Opts) ->
Merged = merge_conf(Key, NewConf),
check_res(Key, emqx_conf:update([Key], Merged, ?OPTIONS), NewConf, Mode);
update_config_cluster(Key, Value, replace = Mode) ->
check_res(Key, emqx_conf:update([Key], Value, ?OPTIONS), Value, Mode).
check_res(Key, emqx_conf:update([Key], Merged, ?OPTIONS), NewConf, Opts);
update_config_cluster(Key, Value, #{mode := replace} = Opts) ->
check_res(Key, emqx_conf:update([Key], Value, ?OPTIONS), Value, Opts).
-define(LOCAL_OPTIONS, #{rawconf_with_defaults => true, persistent => false}).
update_config_local(?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_BINARY = Key, Conf, merge = Mode) ->
check_res(node(), Key, emqx_authz:merge_local(Conf, ?LOCAL_OPTIONS), Conf, Mode);
update_config_local(?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY = Key, Conf, merge = Mode) ->
check_res(node(), Key, emqx_authn:merge_config_local(Conf, ?LOCAL_OPTIONS), Conf, Mode);
update_config_local(Key, NewConf, merge = Mode) ->
update_config_local(
?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_BINARY = Key,
Conf,
#{mode := merge} = Opts
) ->
check_res(node(), Key, emqx_authz:merge_local(Conf, ?LOCAL_OPTIONS), Conf, Opts);
update_config_local(
?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY = Key,
Conf,
#{mode := merge} = Opts
) ->
check_res(node(), Key, emqx_authn:merge_config_local(Conf, ?LOCAL_OPTIONS), Conf, Opts);
update_config_local(Key, NewConf, #{mode := merge} = Opts) ->
Merged = merge_conf(Key, NewConf),
check_res(node(), Key, emqx:update_config([Key], Merged, ?LOCAL_OPTIONS), NewConf, Mode);
update_config_local(Key, Value, replace = Mode) ->
check_res(node(), Key, emqx:update_config([Key], Value, ?LOCAL_OPTIONS), Value, Mode).
check_res(node(), Key, emqx:update_config([Key], Merged, ?LOCAL_OPTIONS), NewConf, Opts);
update_config_local(Key, Value, #{mode := replace} = Opts) ->
check_res(node(), Key, emqx:update_config([Key], Value, ?LOCAL_OPTIONS), Value, Opts).
check_res(Key, Res, Conf, Mode) -> check_res(cluster, Key, Res, Conf, Mode).
check_res(Node, Key, {ok, _}, _Conf, _Mode) ->
emqx_ctl:print("load ~ts on ~p ok~n", [Key, Node]),
check_res(Key, Res, Conf, Opts) -> check_res(cluster, Key, Res, Conf, Opts).
check_res(Node, Key, {ok, _}, _Conf, Opts) ->
print(Opts, "load ~ts on ~p ok~n", [Key, Node]),
ok;
check_res(_Node, Key, {error, Reason}, Conf, Mode) ->
check_res(_Node, Key, {error, Reason}, Conf, Opts = #{mode := Mode}) ->
Warning =
"Can't ~ts the new configurations!~n"
"Root key: ~ts~n"
"Reason: ~p~n",
emqx_ctl:warning(Warning, [Mode, Key, Reason]),
ActiveMsg0 =
"The effective configurations:~n"
"```~n"
@ -285,12 +302,14 @@ check_res(_Node, Key, {error, Reason}, Conf, Mode) ->
"```~n"
"~ts```~n",
FailedMsg = io_lib:format(FailedMsg0, [Mode, hocon_pp:do(#{Key => Conf}, #{})]),
SuggestMsg = suggest_msg(Mode),
SuggestMsg = suggest_msg(Reason, Mode),
Msg = iolist_to_binary([ActiveMsg, FailedMsg, SuggestMsg]),
emqx_ctl:print("~ts", [Msg]),
{error, iolist_to_binary([Warning, Msg])}.
print(Opts, "~ts~n", [Msg]),
warning(Opts, Warning, [Mode, Key, Reason]),
{error, iolist_to_binary([Msg, "\n", io_lib:format(Warning, [Mode, Key, Reason])])}.
suggest_msg(Mode) when Mode == merge orelse Mode == replace ->
%% The mix data failed validation, suggest the user to retry with another mode.
suggest_msg(#{kind := validation_error, reason := unknown_fields}, Mode) ->
RetryMode =
case Mode of
merge -> "replace";
@ -298,9 +317,11 @@ suggest_msg(Mode) when Mode == merge orelse Mode == replace ->
end,
io_lib:format(
"Tips: There may be some conflicts in the new configuration under `~ts` mode,~n"
"Please retry with the `~ts` mode.~n",
"Please retry with the `~ts` mode.",
[Mode, RetryMode]
).
);
suggest_msg(_, _) ->
<<"">>.
check_config(Conf) ->
case check_keys_is_not_readonly(Conf) of
@ -327,19 +348,19 @@ check_config_schema(Conf) ->
sorted_fold(Fold, Conf).
%% @doc Reload etc/emqx.conf to runtime config except for the readonly config
-spec reload_etc_conf_on_local_node(replace | merge) -> ok | {error, term()}.
reload_etc_conf_on_local_node(ReplaceOrMerge) ->
-spec reload_etc_conf_on_local_node(#{mode => replace | merge}) -> ok | {error, term()}.
reload_etc_conf_on_local_node(Opts) ->
case load_etc_config_file() of
{ok, RawConf} ->
case filter_readonly_config(RawConf) of
{ok, Reloaded} ->
reload_config(Reloaded, ReplaceOrMerge);
reload_config(Reloaded, Opts);
{error, Error} ->
emqx_ctl:warning("check config failed~n~p~n", [Error]),
warning(Opts, "check config failed~n~p~n", [Error]),
{error, Error}
end;
{error, Error} ->
emqx_ctl:warning("bad_hocon_file~n ~p~n", [Error]),
warning(Opts, "bad_hocon_file~n ~p~n", [Error]),
{error, bad_hocon_file}
end.
@ -385,9 +406,9 @@ filter_readonly_config(Raw) ->
{error, Error}
end.
reload_config(AllConf, ReplaceOrMerge) ->
reload_config(AllConf, Opts) ->
Fold = fun({Key, Conf}, Acc) ->
case update_config_local(Key, Conf, ReplaceOrMerge) of
case update_config_local(Key, Conf, Opts) of
ok ->
Acc;
Error ->
@ -441,3 +462,9 @@ check_config(SchemaMod, Key, Value) ->
throw:Error ->
{error, Error}
end.
warning(#{log := none}, _, _) -> ok;
warning(_, Format, Args) -> emqx_ctl:warning(Format, Args).
print(#{log := none}, _, _) -> ok;
print(_, Format, Args) -> emqx_ctl:print(Format, Args).

View File

@ -27,11 +27,11 @@ all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
emqx_mgmt_api_test_util:init_suite([emqx_conf, emqx_authz]),
emqx_mgmt_api_test_util:init_suite([emqx_conf, emqx_authz, emqx_authn]),
Config.
end_per_suite(_Config) ->
emqx_mgmt_api_test_util:end_suite([emqx_conf, emqx_authz]).
emqx_mgmt_api_test_util:end_suite([emqx_conf, emqx_authz, emqx_authn]).
t_load_config(Config) ->
Authz = authorization,
@ -64,6 +64,88 @@ t_load_config(Config) ->
?assertEqual({error, empty_hocon_file}, emqx_conf_cli:conf(["load", "non-exist-file"])),
ok.
t_conflict_mix_conf(Config) ->
case emqx_release:edition() of
ce ->
%% Don't fail if the test is run with emqx profile
ok;
ee ->
AuthNInit = emqx_conf:get_raw([authentication]),
Redis = #{
<<"backend">> => <<"redis">>,
<<"cmd">> => <<"HMGET mqtt_user:${username} password_hash salt">>,
<<"enable">> => false,
<<"mechanism">> => <<"password_based">>,
%% password_hash_algorithm {name = sha256, salt_position = suffix}
<<"redis_type">> => <<"single">>,
<<"server">> => <<"127.0.0.1:6379">>
},
AuthN = #{<<"authentication">> => [Redis]},
ConfBin = hocon_pp:do(AuthN, #{}),
ConfFile = prepare_conf_file(?FUNCTION_NAME, ConfBin, Config),
%% init with redis sources
ok = emqx_conf_cli:conf(["load", "--replace", ConfFile]),
?assertMatch([Redis], emqx_conf:get_raw([authentication])),
%% change redis type from single to cluster
%% the server field will become servers field
RedisCluster = maps:remove(<<"server">>, Redis#{
<<"redis_type">> => cluster,
<<"servers">> => [<<"127.0.0.1:6379">>]
}),
AuthN1 = AuthN#{<<"authentication">> => [RedisCluster]},
ConfBin1 = hocon_pp:do(AuthN1, #{}),
ConfFile1 = prepare_conf_file(?FUNCTION_NAME, ConfBin1, Config),
{error, Reason} = emqx_conf_cli:conf(["load", "--merge", ConfFile1]),
?assertNotEqual(
nomatch,
binary:match(
Reason,
[<<"Tips: There may be some conflicts in the new configuration under">>]
),
Reason
),
%% use replace to change redis type from single to cluster
?assertMatch(ok, emqx_conf_cli:conf(["load", "--replace", ConfFile1])),
%% clean up
ConfBinInit = hocon_pp:do(#{<<"authentication">> => AuthNInit}, #{}),
ConfFileInit = prepare_conf_file(?FUNCTION_NAME, ConfBinInit, Config),
ok = emqx_conf_cli:conf(["load", "--replace", ConfFileInit]),
ok
end.
t_config_handler_hook_failed(Config) ->
Listeners =
#{
<<"listeners">> => #{
<<"ssl">> => #{
<<"default">> => #{
<<"ssl_options">> => #{
<<"keyfile">> => <<"">>
}
}
}
}
},
ConfBin = hocon_pp:do(Listeners, #{}),
ConfFile = prepare_conf_file(?FUNCTION_NAME, ConfBin, Config),
{error, Reason} = emqx_conf_cli:conf(["load", "--merge", ConfFile]),
%% the hook failed with empty keyfile
?assertEqual(
nomatch,
binary:match(Reason, [
<<"Tips: There may be some conflicts in the new configuration under">>
]),
Reason
),
?assertNotEqual(
nomatch,
binary:match(Reason, [
<<"{bad_ssl_config,#{reason => pem_file_path_or_string_is_required">>
]),
Reason
),
ok.
t_load_readonly(Config) ->
Base0 = base_conf(),
Base1 = Base0#{<<"mqtt">> => emqx_conf:get_raw([mqtt])},

View File

@ -344,7 +344,7 @@ configs(get, #{query_string := QueryStr, headers := Headers}, _Req) ->
{error, _} = Error -> {400, #{code => 'INVALID_ACCEPT', message => ?ERR_MSG(Error)}}
end;
configs(put, #{body := Conf, query_string := #{<<"mode">> := Mode}}, _Req) ->
case emqx_conf_cli:load_config(Conf, Mode) of
case emqx_conf_cli:load_config(Conf, #{mode => Mode, log => none}) of
ok -> {200};
{error, Msg} -> {400, #{<<"content-type">> => <<"text/plain">>}, Msg}
end.

View File

@ -0,0 +1 @@
Fixes misunderstood prompt when invalid certificates/keys were specified for the `/configs` API.