Skip to content

Introduce a limit for how many virtual hosts can be created in a cluster #7798

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions deps/rabbit/priv/schema/rabbit.schema
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,19 @@ end}.
end
}.

{mapping, "vhost_max", "rabbit.vhost_max",
[{datatype, [{atom, infinity}, integer]}, {validators, ["non_negative_integer"]}]}.

{translation, "rabbit.vhost_max",
fun(Conf) ->
case cuttlefish:conf_get("vhost_max", Conf, undefined) of
undefined -> cuttlefish:unset();
infinity -> infinity;
Val when is_integer(Val) -> Val;
_ -> cuttlefish:invalid("should be a non-negative integer")
end
end
}.

{mapping, "max_message_size", "rabbit.max_message_size",
[{datatype, integer}, {validators, ["max_message_size"]}]}.
Expand Down
2 changes: 2 additions & 0 deletions deps/rabbit/src/rabbit_queue_type.erl
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
-type arguments() :: queue_arguments | consumer_arguments.
-type queue_type() :: rabbit_classic_queue | rabbit_quorum_queue | rabbit_stream_queue.

-export_type([queue_type/0]).

-define(STATE, ?MODULE).

%% Recoverable mirrors shouldn't really be a generic one, but let's keep it here until
Expand Down
35 changes: 35 additions & 0 deletions deps/rabbit/src/rabbit_vhost.erl
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ add(Name, Metadata, ActingUser) ->
end.

do_add(Name, Metadata, ActingUser) ->
ok = is_over_vhost_limit(Name),
Description = maps:get(description, Metadata, undefined),
Tags = maps:get(tags, Metadata, []),

Expand Down Expand Up @@ -194,6 +195,7 @@ do_add(Name, Metadata, ActingUser) ->
[Name, Description, Tags])
end,
DefaultLimits = rabbit_db_vhost_defaults:list_limits(Name),

{NewOrNot, VHost} = rabbit_db_vhost:create_or_get(Name, DefaultLimits, Metadata),
case NewOrNot of
new ->
Expand Down Expand Up @@ -284,9 +286,22 @@ delete(VHost, ActingUser) ->
rabbit_vhost_sup_sup:delete_on_all_nodes(VHost),
ok.

-spec put_vhost(vhost:name(),
binary(),
vhost:tags(),
boolean(),
rabbit_types:username()) ->
'ok' | {'error', any()} | {'EXIT', any()}.
put_vhost(Name, Description, Tags0, Trace, Username) ->
put_vhost(Name, Description, Tags0, undefined, Trace, Username).

-spec put_vhost(vhost:name(),
binary(),
vhost:unparsed_tags() | vhost:tags(),
rabbit_queue_type:queue_type() | 'undefined',
boolean(),
rabbit_types:username()) ->
'ok' | {'error', any()} | {'EXIT', any()}.
put_vhost(Name, Description, Tags0, DefaultQueueType, Trace, Username) ->
Tags = case Tags0 of
undefined -> <<"">>;
Expand Down Expand Up @@ -331,6 +346,26 @@ put_vhost(Name, Description, Tags0, DefaultQueueType, Trace, Username) ->
end,
Result.

-spec is_over_vhost_limit(vhost:name()) -> 'ok' | no_return().
is_over_vhost_limit(Name) ->
Limit = rabbit_misc:get_env(rabbit, vhost_max, infinity),
is_over_vhost_limit(Name, Limit).

-spec is_over_vhost_limit(vhost:name(), 'infinity' | non_neg_integer())
-> 'ok' | no_return().
is_over_vhost_limit(_Name, infinity) ->
ok;
is_over_vhost_limit(Name, Limit) when is_integer(Limit) ->
case length(rabbit_db_vhost:list()) >= Limit of
false ->
ok;
true ->
ErrorMsg = rabbit_misc:format("cannot create vhost '~ts': "
"vhost limit of ~tp is reached",
[Name, Limit]),
exit({vhost_limit_exceeded, ErrorMsg})
end.

%% when definitions are loaded on boot, Username here will be ?INTERNAL_USER,
%% which does not actually exist
maybe_grant_full_permissions(_Name, ?INTERNAL_USER) ->
Expand Down
4 changes: 4 additions & 0 deletions deps/rabbit/src/vhost.erl
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@

-type(description() :: binary()).
-type(tag() :: atom()).
-type(tags() :: [tag()]).
-type(unparsed_tags() :: binary() | string() | atom()).

-type vhost() :: vhost_v2().

Expand Down Expand Up @@ -76,6 +78,8 @@
metadata/0,
description/0,
tag/0,
unparsed_tags/0,
tags/0,
vhost/0,
vhost_v2/0,
vhost_pattern/0,
Expand Down
31 changes: 29 additions & 2 deletions deps/rabbit/test/per_node_limit_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ all() ->
groups() ->
[
{parallel_tests, [parallel], [
node_connection_limit
node_connection_limit,
vhost_limit
]}
].

Expand Down Expand Up @@ -58,6 +59,9 @@ end_per_group(_Group, Config) ->
init_per_testcase(Testcase, Config) ->
rabbit_ct_helpers:testcase_started(Config, Testcase).

end_per_testcase(vhost_limit = Testcase, Config) ->
[rabbit_ct_broker_helpers:delete_vhost(Config, integer_to_binary(I)) || I <- lists:seq(1,4)],
rabbit_ct_helpers:testcase_finished(Config, Testcase);
end_per_testcase(Testcase, Config) ->
rabbit_ct_helpers:testcase_finished(Config, Testcase).

Expand All @@ -78,7 +82,25 @@ node_connection_limit(Config) ->

set_node_limit(Config, infinity),
C = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 0),
true = is_pid(C).
true = is_pid(C),
close_all_connections([C]),
ok.

vhost_limit(Config) ->
set_vhost_limit(Config, 0),
{'EXIT',{vhost_limit_exceeded, _}} = rabbit_ct_broker_helpers:add_vhost(Config, <<"foo">>),

set_vhost_limit(Config, 5),
[ok = rabbit_ct_broker_helpers:add_vhost(Config, integer_to_binary(I)) || I <- lists:seq(1,4)],
{'EXIT',{vhost_limit_exceeded, _}} = rabbit_ct_broker_helpers:add_vhost(Config, <<"5">>),
[rabbit_ct_broker_helpers:delete_vhost(Config, integer_to_binary(I)) || I <- lists:seq(1,4)],

set_vhost_limit(Config, infinity),
[ok = rabbit_ct_broker_helpers:add_vhost(Config, integer_to_binary(I)) || I <- lists:seq(1,4)],
ok = rabbit_ct_broker_helpers:add_vhost(Config, <<"5">>),
[rabbit_ct_broker_helpers:delete_vhost(Config, integer_to_binary(I)) || I <- lists:seq(1,5)],
ok.


%% -------------------------------------------------------------------
%% Implementation
Expand All @@ -97,3 +119,8 @@ set_node_limit(Config, Limit) ->
rabbit_ct_broker_helpers:rpc(Config, 0,
application,
set_env, [rabbit, connection_max, Limit]).

set_vhost_limit(Config, Limit) ->
rabbit_ct_broker_helpers:rpc(Config, 0,
application,
set_env, [rabbit, vhost_max, Limit]).
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ defmodule RabbitMQ.CLI.Ctl.Commands.AddVhostCommand do
{:error, ExitCodes.exit_usage(), "Unsupported default queue type"}
end

def output({:badrpc, {:EXIT, {:vhost_limit_exceeded, msg}}}, _opts) do
{:error, ExitCodes.exit_usage(), msg}
end

use RabbitMQ.CLI.DefaultOutput

def usage,
Expand Down
47 changes: 25 additions & 22 deletions deps/rabbitmq_management/src/rabbit_mgmt_wm_vhost.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
-export([init/2, resource_exists/2, to_json/2,
content_types_provided/2, content_types_accepted/2,
is_authorized/2, allowed_methods/2, accept_content/2,
delete_resource/2, id/1, put_vhost/6]).
delete_resource/2, id/1]).
-export([variances/2]).

-import(rabbit_misc, [pget/2]).

-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl").
-include_lib("rabbit_common/include/rabbit.hrl").

-dialyzer({nowarn_function, accept_content/2}).

%%--------------------------------------------------------------------

init(Req, _State) ->
Expand Down Expand Up @@ -60,25 +62,28 @@ accept_content(ReqData0, Context = #context{user = #user{username = Username}})
rabbit_mgmt_util:with_decode(
[], ReqData0, Context,
fun(_, BodyMap, ReqData) ->
Trace = rabbit_mgmt_util:parse_bool(maps:get(tracing, BodyMap, undefined)),
Description = maps:get(description, BodyMap, <<"">>),
Tags = maps:get(tags, BodyMap, <<"">>),
%% defaultqueuetype was an unfortunate name picked originally for 3.11.0,
%% so fall back to it. See rabbitmq/rabbitmq-server#7734.
FallbackQT = maps:get(defaultqueuetype, BodyMap, undefined),
DefaultQT = maps:get(default_queue_type, BodyMap, FallbackQT),
case put_vhost(Name, Description, Tags, DefaultQT, Trace, Username) of
ok ->
{true, ReqData, Context};
{error, timeout} = E ->
rabbit_mgmt_util:internal_server_error(
"Timed out while waiting for the vhost to initialise", E,
ReqData0, Context);
{error, E} ->
rabbit_mgmt_util:internal_server_error(
"Error occured while adding vhost", E,
ReqData0, Context)
end
Trace = rabbit_mgmt_util:parse_bool(maps:get(tracing, BodyMap, undefined)),
Description = maps:get(description, BodyMap, <<"">>),
Tags = maps:get(tags, BodyMap, <<"">>),
%% defaultqueuetype was an unfortunate name picked originally for 3.11.0,
%% so fall back to it. See rabbitmq/rabbitmq-server#7734.
FallbackQT = maps:get(defaultqueuetype, BodyMap, undefined),
DefaultQT = maps:get(default_queue_type, BodyMap, FallbackQT),
case rabbit_vhost:put_vhost(Name, Description, Tags, DefaultQT, Trace, Username) of
ok ->
{true, ReqData, Context};
{error, timeout} = E ->
rabbit_mgmt_util:internal_server_error(
"Timed out while waiting for the vhost to initialise", E,
ReqData0, Context);
{error, E} ->
rabbit_mgmt_util:internal_server_error(
"Error occured while adding vhost", E,
ReqData0, Context);
{'EXIT', {vhost_limit_exceeded,
Explanation}} ->
rabbit_mgmt_util:bad_request(list_to_binary(Explanation), ReqData, Context)
end
end).

delete_resource(ReqData, Context = #context{user = #user{username = Username}}) ->
Expand All @@ -101,5 +106,3 @@ id(ReqData) ->
Value -> Value
end.

put_vhost(Name, Description, Tags, DefaultQT, Trace, Username) ->
rabbit_vhost:put_vhost(Name, Description, Tags, DefaultQT, Trace, Username).