Skip to content

Introduce a way to update virtual host metadata using CLI tools #7914

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 10 commits into from
Apr 17, 2023
21 changes: 16 additions & 5 deletions deps/rabbit/src/rabbit_vhost.erl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
set_limits/2, vhost_cluster_state/1, is_running_on_all_nodes/1, await_running_on_all_nodes/2,
list/0, count/0, list_names/0, all/0, all_tagged_with/1]).
-export([parse_tags/1, update_tags/3]).
-export([update_metadata/3]).
-export([lookup/1, default_name/0]).
-export([info/1, info/2, info_all/0, info_all/1, info_all/2, info_all/3]).
-export([dir/1, msg_store_dir_path/1, msg_store_dir_wildcard/0, config_file_path/1, ensure_config_file/1]).
Expand All @@ -28,8 +29,8 @@
%% API
%%

%% this module deals with user inputs, so accepts more than just atoms
-type vhost_tag() :: atom() | string() | binary().
-export_type([vhost_tag/0]).

recover() ->
%% Clear out remnants of old incarnation, in case we restarted
Expand Down Expand Up @@ -231,21 +232,31 @@ do_add(Name, Metadata, ActingUser) ->
{error, Msg}
end.

-spec update(vhost:name(), binary(), [atom()], rabbit_types:username()) -> rabbit_types:ok_or_error(any()).
update(Name, Description, Tags, ActingUser) ->
Metadata = #{description => Description, tags => Tags},
-spec update_metadata(vhost:name(), vhost:metadata(), rabbit_types:username()) -> rabbit_types:ok_or_error(any()).
update_metadata(Name, Metadata0, ActingUser) ->
Metadata = maps:with([description, tags, default_queue_type], Metadata0),

case rabbit_db_vhost:merge_metadata(Name, Metadata) of
{ok, VHost} ->
Description = vhost:get_description(VHost),
Tags = vhost:get_tags(VHost),
DefaultQueueType = vhost:get_default_queue_type(VHost),
rabbit_event:notify(
vhost_updated,
info(VHost) ++ [{user_who_performed_action, ActingUser},
{description, Description},
{tags, Tags}]),
{tags, Tags},
{default_queue_type, DefaultQueueType}]),
ok;
{error, _} = Error ->
Error
end.

-spec update(vhost:name(), binary(), [atom()], rabbit_types:username()) -> rabbit_types:ok_or_error(any()).
update(Name, Description, Tags, ActingUser) ->
Metadata = #{description => Description, tags => Tags},
update_metadata(Name, Metadata, ActingUser).

-spec delete(vhost:name(), rabbit_types:username()) -> rabbit_types:ok_or_error(any()).

delete(VHost, ActingUser) ->
Expand Down
18 changes: 18 additions & 0 deletions deps/rabbitmq_cli/lib/rabbitmq/cli/core/virtual_hosts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## This Source Code Form is subject to the terms of the Mozilla Public
## License, v. 2.0. If a copy of the MPL was not distributed with this
## file, You can obtain one at https://mozilla.org/MPL/2.0/.
##
## Copyright (c) 2007-2023 VMware, Inc. or its affiliates. All rights reserved.
defmodule RabbitMQ.CLI.Core.VirtualHosts do
def parse_tags(tags) do
case tags do
nil ->
nil

val ->
String.split(val, ",", trim: true)
|> Enum.map(&String.trim/1)
|> Enum.map(&String.to_atom/1)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
## Copyright (c) 2007-2023 VMware, Inc. or its affiliates. All rights reserved.

defmodule RabbitMQ.CLI.Ctl.Commands.AddVhostCommand do
alias RabbitMQ.CLI.Core.{DocGuide, ExitCodes, Helpers}
alias RabbitMQ.CLI.Core.{DocGuide, ExitCodes, Helpers, VirtualHosts}

@behaviour RabbitMQ.CLI.CommandBehaviour

Expand All @@ -25,7 +25,11 @@ defmodule RabbitMQ.CLI.Ctl.Commands.AddVhostCommand do
tags: tags,
default_queue_type: default_qt
}) do
meta = %{description: desc, tags: parse_tags(tags), default_queue_type: default_qt}
meta = %{
description: desc,
tags: VirtualHosts.parse_tags(tags),
default_queue_type: default_qt
}

:rabbit_misc.rpc_call(node_name, :rabbit_vhost, :add, [
vhost,
Expand All @@ -38,7 +42,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.AddVhostCommand do
:rabbit_misc.rpc_call(node_name, :rabbit_vhost, :add, [
vhost,
desc,
parse_tags(tags),
VirtualHosts.parse_tags(tags),
Helpers.cli_acting_user()
])
end
Expand Down Expand Up @@ -84,14 +88,4 @@ defmodule RabbitMQ.CLI.Ctl.Commands.AddVhostCommand do
def description(), do: "Creates a virtual host"

def banner([vhost], _), do: "Adding vhost \"#{vhost}\" ..."

#
# Implementation
#

def parse_tags(tags) do
String.split(tags, ",", trim: true)
|> Enum.map(&String.trim/1)
|> Enum.map(&String.to_atom/1)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
## This Source Code Form is subject to the terms of the Mozilla Public
## License, v. 2.0. If a copy of the MPL was not distributed with this
## file, You can obtain one at https://mozilla.org/MPL/2.0/.
##
## Copyright (c) 2007-2023 VMware, Inc. or its affiliates. All rights reserved.

defmodule RabbitMQ.CLI.Ctl.Commands.UpdateVhostMetadataCommand do
alias RabbitMQ.CLI.Core.{DocGuide, ExitCodes, Helpers, VirtualHosts}

@behaviour RabbitMQ.CLI.CommandBehaviour

@metadata_keys [:description, :tags, :default_queue_type]

def switches(), do: [description: :string, tags: :string, default_queue_type: :string]
def aliases(), do: [d: :description]

def merge_defaults(args, opts) do
{args, opts}
end

use RabbitMQ.CLI.Core.RequiresRabbitAppRunning

def validate(args, _) when length(args) == 0 do
{:validation_failure, :not_enough_args}
end

def validate(args, _) when length(args) > 1 do
{:validation_failure, :too_many_args}
end

def validate([_vhost], opts) do
m = :maps.with(@metadata_keys, opts)

case map_size(m) do
0 ->
{:validation_failure, :not_enough_args}

_ ->
# description and tags can be anything but default queue type must
# be a value from a known set
case m[:default_queue_type] do
nil ->
:ok

"quorum" ->
:ok

"stream" ->
:ok

"classic" ->
:ok

other ->
{:validation_failure,
{:bad_argument,
"Default queue type must be one of: quorum, stream, classic. Provided: #{other}"}}
end
end
end

def validate(_, _), do: :ok

def run([vhost], %{node: node_name} = opts) do
meta = :maps.with(@metadata_keys, opts)
tags = meta[:tags]

meta =
case tags do
nil -> meta
other -> %{meta | tags: VirtualHosts.parse_tags(other)}
end

:rabbit_misc.rpc_call(node_name, :rabbit_vhost, :update_metadata, [
vhost,
meta,
Helpers.cli_acting_user()
])
end

def output({:error, :invalid_queue_type}, _opts) do
{:error, ExitCodes.exit_usage(), "Unsupported default queue type"}
end

use RabbitMQ.CLI.DefaultOutput

def usage,
do:
"update_vhost_metadata <vhost> [--description <description>] [--tags \"<tag1>,<tag2>,<...>\"] [--default-queue-type <quorum|classic|stream>]"

def usage_additional() do
[
["<vhost>", "Virtual host name"],
["--description <description>", "Virtual host description"],
["--tags <tag1,tag2>", "Comma-separated list of tags"],
[
"--default-queue-type <quorum|classic|stream>",
"Queue type to use if no type is explicitly provided by the client"
]
]
end

def usage_doc_guides() do
[
DocGuide.virtual_hosts()
]
end

def help_section(), do: :virtual_hosts

def description(), do: "Updates metadata (tags, description, default queue type) a virtual host"

def banner([vhost], _), do: "Updating metadata of vhost \"#{vhost}\" ..."
end
2 changes: 1 addition & 1 deletion deps/rabbitmq_cli/test/ctl/add_vhost_command_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ defmodule AddVhostCommandTest do
end

@tag vhost: @vhost
test "run: vhost tags are conformed to a list", context do
test "run: vhost tags are coerced to a list", context do
opts = Map.merge(context[:opts], %{description: "My vhost", tags: "my_tag"})
assert @command.run([context[:vhost]], opts) == :ok
record = list_vhosts() |> Enum.find(fn record -> record[:name] == context[:vhost] end)
Expand Down
111 changes: 111 additions & 0 deletions deps/rabbitmq_cli/test/ctl/update_vhost_metadata_command_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
## This Source Code Form is subject to the terms of the Mozilla Public
## License, v. 2.0. If a copy of the MPL was not distributed with this
## file, You can obtain one at https://mozilla.org/MPL/2.0/.
##
## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved.

defmodule UpdateVhostMetadataCommandTest do
use ExUnit.Case, async: false
import TestHelper

@command RabbitMQ.CLI.Ctl.Commands.UpdateVhostMetadataCommand
@vhost "update-metadata-test"

setup_all do
RabbitMQ.CLI.Core.Distribution.start()
{:ok, opts: %{node: get_rabbit_hostname()}}
end

setup context do
on_exit(context, fn -> delete_vhost(context[:vhost]) end)
:ok
end

test "validate: no arguments fails validation" do
assert @command.validate([], %{}) == {:validation_failure, :not_enough_args}
end

test "validate: too many arguments fails validation" do
assert @command.validate(["test", "extra"], %{}) == {:validation_failure, :too_many_args}
end

test "validate: virtual host name without options fails validation" do
assert @command.validate(["a-vhost"], %{}) == {:validation_failure, :not_enough_args}
end

test "validate: virtual host name and one or more metadata options succeeds" do
assert @command.validate(["a-vhost"], %{description: "Used by team A"}) == :ok

assert @command.validate(["a-vhost"], %{
description: "Used by team A for QA purposes",
tags: "qa,team-a"
}) == :ok

assert @command.validate(["a-vhost"], %{
description: "Used by team A for QA purposes",
tags: "qa,team-a",
default_queue_type: "quorum"
}) == :ok
end

test "validate: unknown default queue type fails validation" do
assert @command.validate(["a-vhost"], %{
description: "Used by team A for QA purposes",
tags: "qa,team-a",
default_queue_type: "unknown"
}) ==
{:validation_failure,
{:bad_argument,
"Default queue type must be one of: quorum, stream, classic. Provided: unknown"}}
end

test "run: passing a valid vhost name and description succeeds", context do
add_vhost(@vhost)
desc = "desc 2"

assert @command.run([@vhost], Map.merge(context[:opts], %{description: desc})) == :ok
vh = find_vhost(@vhost)

assert vh
assert vh[:description] == desc
end

test "run: passing a valid vhost name and a set of tags succeeds", context do
add_vhost(@vhost)
tags = "a1,b2,c3"

assert @command.run([@vhost], Map.merge(context[:opts], %{tags: tags})) == :ok
vh = find_vhost(@vhost)

assert vh
assert vh[:tags] == [:a1, :b2, :c3]
end

test "run: attempt to use a non-existent virtual host fails", context do
vh = "a-non-existent-3882-vhost"

assert match?(
{:error, {:no_such_vhost, _}},
@command.run([vh], Map.merge(context[:opts], %{description: "irrelevant"}))
)
end

test "run: attempt to use an unreachable node returns a nodedown" do
opts = %{node: :jake@thedog, timeout: 200, description: "does not matter"}
assert match?({:badrpc, _}, @command.run(["na"], opts))
end

test "run: vhost tags are coerced to a list", context do
add_vhost(@vhost)

opts = Map.merge(context[:opts], %{description: "My vhost", tags: "my_tag"})
assert @command.run([@vhost], opts) == :ok
vh = find_vhost(@vhost)
assert vh[:tags] == [:my_tag]
end

test "banner", context do
assert @command.banner([@vhost], context[:opts]) =~
~r/Updating metadata of vhost/
end
end
15 changes: 13 additions & 2 deletions deps/rabbitmq_cli/test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,19 @@ defmodule TestHelper do
:rpc.call(get_rabbit_hostname(), :rabbit_nodes, :cluster_name, [])
end

def add_vhost(name) do
:rpc.call(get_rabbit_hostname(), :rabbit_vhost, :add, [name, "acting-user"])
def add_vhost(name, meta \\ %{}) do
:rpc.call(get_rabbit_hostname(), :rabbit_vhost, :add, [name, meta, "acting-user"])
end

def find_vhost(name) do
case :rpc.call(get_rabbit_hostname(), :rabbit_vhost, :lookup, [name]) do
{:error, _} = err ->
err

vhost_rec ->
{:vhost, _name, limits, meta} = vhost_rec
Map.merge(meta, %{name: name, limits: limits})
end
end

def delete_vhost(name) do
Expand Down