From a3637373376017eeb710304b8872fd0faea3fb5d Mon Sep 17 00:00:00 2001 From: Diana Parra Corbacho Date: Fri, 10 Nov 2023 10:50:34 +0100 Subject: [PATCH] CLI: list_deprecated_features command Lists all or used deprecated features --- deps/rabbit/app.bzl | 3 + deps/rabbit/src/rabbit_depr_ff_extra.erl | 69 +++++++++ .../rabbit/src/rabbit_deprecated_features.erl | 66 +++++--- .../rabbit/test/deprecated_features_SUITE.erl | 53 ++++++- .../list_deprecated_features_command.ex | 123 +++++++++++++++ .../list_deprecated_features_command_test.exs | 145 ++++++++++++++++++ 6 files changed, 440 insertions(+), 19 deletions(-) create mode 100644 deps/rabbit/src/rabbit_depr_ff_extra.erl create mode 100644 deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/list_deprecated_features_command.ex create mode 100644 deps/rabbitmq_cli/test/ctl/list_deprecated_features_command_test.exs diff --git a/deps/rabbit/app.bzl b/deps/rabbit/app.bzl index 13ff05c6a2f6..ced0c3bb83e4 100644 --- a/deps/rabbit/app.bzl +++ b/deps/rabbit/app.bzl @@ -110,6 +110,7 @@ def all_beam_files(name = "all_beam_files"): "src/rabbit_definitions_hashing.erl", "src/rabbit_definitions_import_https.erl", "src/rabbit_definitions_import_local_filesystem.erl", + "src/rabbit_depr_ff_extra.erl", "src/rabbit_deprecated_features.erl", "src/rabbit_diagnostics.erl", "src/rabbit_direct.erl", @@ -373,6 +374,7 @@ def all_test_beam_files(name = "all_test_beam_files"): "src/rabbit_definitions_hashing.erl", "src/rabbit_definitions_import_https.erl", "src/rabbit_definitions_import_local_filesystem.erl", + "src/rabbit_depr_ff_extra.erl", "src/rabbit_deprecated_features.erl", "src/rabbit_diagnostics.erl", "src/rabbit_direct.erl", @@ -652,6 +654,7 @@ def all_srcs(name = "all_srcs"): "src/rabbit_definitions_hashing.erl", "src/rabbit_definitions_import_https.erl", "src/rabbit_definitions_import_local_filesystem.erl", + "src/rabbit_depr_ff_extra.erl", "src/rabbit_deprecated_features.erl", "src/rabbit_diagnostics.erl", "src/rabbit_direct.erl", diff --git a/deps/rabbit/src/rabbit_depr_ff_extra.erl b/deps/rabbit/src/rabbit_depr_ff_extra.erl new file mode 100644 index 000000000000..5267c3efbfb6 --- /dev/null +++ b/deps/rabbit/src/rabbit_depr_ff_extra.erl @@ -0,0 +1,69 @@ +%% 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) 2023 Broadcom. All Rights Reserved. The term “Broadcom” +%% refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. +%% +%% @doc +%% This module provides extra functions unused by the feature flags +%% subsystem core functionality. + +-module(rabbit_depr_ff_extra). + +-export([cli_info/1]). + +-type cli_info() :: [cli_info_entry()]. +%% A list of deprecated feature properties, formatted for the RabbitMQ CLI. + +-type cli_info_entry() :: + #{name => rabbit_feature_flags:feature_name(), + deprecation_phase => rabbit_deprecated_features:deprecation_phase(), + provided_by => atom(), + desc => string(), + doc_url => string()}. +%% A list of properties for a single deprecated feature, formatted for the +%% RabbitMQ CLI. + +-spec cli_info(Which) -> CliInfo when + Which :: all | used, + CliInfo :: cli_info(). +%% @doc +%% Returns a list of all or used deprecated features properties, +%% depending on the argument. +%% +%% @param Which The group of deprecated features to return: `all' or `used'. +%% @returns the list of all deprecated feature properties. + +cli_info(all) -> + cli_info0(rabbit_deprecated_features:list(all)); +cli_info(used) -> + cli_info0(rabbit_deprecated_features:list(used)). + +-spec cli_info0(FeatureFlags) -> CliInfo when + FeatureFlags :: rabbit_feature_flags:feature_flags(), + CliInfo :: cli_info(). +%% @doc +%% Formats a map of deprecated features and their properties into a list of +%% deprecated feature properties as expected by the RabbitMQ CLI. +%% +%% @param DeprecatedFeatures A map of deprecated features. +%% @returns the list of deprecated features properties, created from the map +%% specified in arguments. + +cli_info0(DeprecatedFeature) -> + lists:foldr( + fun(FeatureName, Acc) -> + FeatureProps = maps:get(FeatureName, DeprecatedFeature), + + App = maps:get(provided_by, FeatureProps), + DeprecationPhase = maps:get(deprecation_phase, FeatureProps, ""), + Desc = maps:get(desc, FeatureProps, ""), + DocUrl = maps:get(doc_url, FeatureProps, ""), + Info = #{name => FeatureName, + desc => unicode:characters_to_binary(Desc), + deprecation_phase => DeprecationPhase, + doc_url => unicode:characters_to_binary(DocUrl), + provided_by => App}, + [Info | Acc] + end, [], lists:sort(maps:keys(DeprecatedFeature))). diff --git a/deps/rabbit/src/rabbit_deprecated_features.erl b/deps/rabbit/src/rabbit_deprecated_features.erl index 263c4548e99b..5254dd2f8fde 100644 --- a/deps/rabbit/src/rabbit_deprecated_features.erl +++ b/deps/rabbit/src/rabbit_deprecated_features.erl @@ -117,7 +117,8 @@ get_warning/1]). -export([extend_properties/2, should_be_permitted/2, - enable_underlying_feature_flag_cb/1]). + enable_underlying_feature_flag_cb/1, + list/1]). -type deprecated_feature_modattr() :: {rabbit_feature_flags:feature_name(), feature_props()}. @@ -202,6 +203,10 @@ %% needed. Other added properties are the same as {@link %% rabbit_feature_flags:feature_props_extended()}. +-type deprecated_features() :: + #{rabbit_feature_flags:feature_name() => + feature_props_extended()}. + -type callbacks() :: is_feature_used_callback(). %% All possible callbacks. @@ -346,6 +351,28 @@ get_warning(FeatureProps, Permitted) when is_map(FeatureProps) -> maps:get(when_removed, Msgs) end. +-spec list(Which :: all | used) -> deprecated_features(). +%% @doc +%% Lists all or used deprecated features, depending on the argument. +%% +%% @param Which The group of deprecated features to return: `all' or `used'. +%% @returns A map of selected deprecated features. + +list(all) -> + maps:filter( + fun(_, FeatureProps) -> ?IS_DEPRECATION(FeatureProps) end, + rabbit_ff_registry_wrapper:list(all)); +list(used) -> + maps:filter( + fun(FeatureName, FeatureProps) -> + ?IS_DEPRECATION(FeatureProps) + and + is_deprecated_feature_in_use( + #{feature_name => FeatureName, + feature_props => FeatureProps}) =:= true + end, + rabbit_ff_registry_wrapper:list(all)). + %% ------------------------------------------------------------------- %% Internal functions. %% ------------------------------------------------------------------- @@ -581,24 +608,27 @@ should_log_warning(FeatureName) -> enable_underlying_feature_flag_cb( #{command := enable, - feature_name := FeatureName, - feature_props := #{callbacks := Callbacks}} = Args) -> + feature_name := FeatureName} = Args) -> + IsUsed = is_deprecated_feature_in_use(Args), + case IsUsed of + true -> + ?LOG_ERROR( + "Deprecated features: `~ts`: can't deny deprecated " + "feature because it is actively used", + [FeatureName], + #{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}), + {error, + {failed_to_deny_deprecated_features, [FeatureName]}}; + _ -> + ok + end. + +is_deprecated_feature_in_use( + #{feature_props := #{callbacks := Callbacks}} = Args1) -> case Callbacks of #{is_feature_used := {CallbackMod, CallbackFun}} -> - Args1 = Args#{command => is_feature_used}, - IsUsed = erlang:apply(CallbackMod, CallbackFun, [Args1]), - case IsUsed of - false -> - ok; - true -> - ?LOG_ERROR( - "Deprecated features: `~ts`: can't deny deprecated " - "feature because it is actively used", - [FeatureName], - #{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}), - {error, - {failed_to_deny_deprecated_features, [FeatureName]}} - end; + Args = Args1#{command => is_feature_used}, + erlang:apply(CallbackMod, CallbackFun, [Args]); _ -> - ok + undefined end. diff --git a/deps/rabbit/test/deprecated_features_SUITE.erl b/deps/rabbit/test/deprecated_features_SUITE.erl index 603f9a88fe05..669a45d98cf8 100644 --- a/deps/rabbit/test/deprecated_features_SUITE.erl +++ b/deps/rabbit/test/deprecated_features_SUITE.erl @@ -36,6 +36,8 @@ get_appropriate_warning_when_disconnected/1, get_appropriate_warning_when_removed/1, deprecated_feature_enabled_if_feature_flag_depends_on_it/1, + list_all_deprecated_features/1, + list_used_deprecated_features/1, feature_is_unused/1, feature_is_used/1 @@ -67,7 +69,9 @@ groups() -> get_appropriate_warning_when_denied, get_appropriate_warning_when_disconnected, get_appropriate_warning_when_removed, - deprecated_feature_enabled_if_feature_flag_depends_on_it + deprecated_feature_enabled_if_feature_flag_depends_on_it, + list_all_deprecated_features, + list_used_deprecated_features ], [ {cluster_size_1, [], Tests}, @@ -726,3 +730,50 @@ deprecated_feature_enabled_if_feature_flag_depends_on_it(Config) -> ok end ) || Node <- AllNodes]. + +list_all_deprecated_features(Config) -> + [FirstNode | _] = AllNodes = ?config(nodes, Config), + feature_flags_v2_SUITE:connect_nodes(AllNodes), + feature_flags_v2_SUITE:override_running_nodes(AllNodes), + + FeatureName = ?FUNCTION_NAME, + FeatureFlags = #{FeatureName => + #{provided_by => rabbit, + deprecation_phase => permitted_by_default}}, + ?assertEqual( + ok, + feature_flags_v2_SUITE:inject_on_nodes(AllNodes, FeatureFlags)), + + feature_flags_v2_SUITE:run_on_node( + FirstNode, + fun() -> + Map = rabbit_deprecated_features:list(all), + ?assert(maps:is_key(FeatureName, Map)) + end). + +list_used_deprecated_features(Config) -> + [FirstNode | _] = AllNodes = ?config(nodes, Config), + feature_flags_v2_SUITE:connect_nodes(AllNodes), + feature_flags_v2_SUITE:override_running_nodes(AllNodes), + + UsedFeatureName = used_deprecated_feature, + UnusedFeatureName = unused_deprecated_feature, + FeatureFlags = #{UsedFeatureName => + #{provided_by => rabbit, + deprecation_phase => permitted_by_default, + callbacks => #{is_feature_used => {?MODULE, feature_is_used}}}, + UnusedFeatureName => + #{provided_by => rabbit, + deprecation_phase => permitted_by_default, + callbacks => #{is_feature_used => {?MODULE, feature_is_unused}}}}, + ?assertEqual( + ok, + feature_flags_v2_SUITE:inject_on_nodes(AllNodes, FeatureFlags)), + + feature_flags_v2_SUITE:run_on_node( + FirstNode, + fun() -> + Map = rabbit_deprecated_features:list(used), + ?assertNot(maps:is_key(UnusedFeatureName, Map)), + ?assert(maps:is_key(UsedFeatureName, Map)) + end). diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/list_deprecated_features_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/list_deprecated_features_command.ex new file mode 100644 index 000000000000..e1845cce274f --- /dev/null +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/list_deprecated_features_command.ex @@ -0,0 +1,123 @@ +## 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) 2023 Broadcom. All Rights Reserved. The term “Broadcom” +## refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. + +defmodule RabbitMQ.CLI.Ctl.Commands.ListDeprecatedFeaturesCommand do + alias RabbitMQ.CLI.Core.{DocGuide, Validators} + alias RabbitMQ.CLI.Ctl.InfoKeys + + @behaviour RabbitMQ.CLI.CommandBehaviour + use RabbitMQ.CLI.DefaultOutput + + def formatter(), do: RabbitMQ.CLI.Formatters.Table + + @info_keys ~w(name deprecation_phase provided_by desc doc_url)a + + def info_keys(), do: @info_keys + + def scopes(), do: [:ctl, :diagnostics] + + def switches(), do: [used: :boolean] + + def merge_defaults([], opts) do + {["name", "deprecation_phase"], Map.merge(%{used: false}, opts)} + end + + def merge_defaults(args, opts) do + {args, Map.merge(%{used: false}, opts)} + end + + def validate(args, _) do + case InfoKeys.validate_info_keys(args, @info_keys) do + {:ok, _} -> :ok + err -> err + end + end + + def validate_execution_environment(args, opts) do + Validators.chain( + [ + &Validators.rabbit_is_loaded/2, + &Validators.rabbit_is_running/2 + ], + [args, opts] + ) + end + + def run([_ | _] = args, %{node: node_name, timeout: timeout, used: false}) do + case :rabbit_misc.rpc_call( + node_name, + :rabbit_depr_ff_extra, + :cli_info, + [:all], + timeout + ) do + # Server does not support deprecated features, consider none are available. + {:badrpc, {:EXIT, {:undef, _}}} -> [] + {:badrpc, _} = err -> err + val -> filter_by_arg(val, args) + end + end + + def run([_ | _] = args, %{node: node_name, timeout: timeout, used: true}) do + case :rabbit_misc.rpc_call( + node_name, + :rabbit_deprecated_feature_extra, + :cli_info, + [:used], + timeout + ) do + # Server does not support deprecated features, consider none are available. + {:badrpc, {:EXIT, {:undef, _}}} -> [] + {:badrpc, _} = err -> err + val -> filter_by_arg(val, args) + end + end + + def banner(_, %{used: false}), do: "Listing deprecated features ..." + def banner(_, %{used: true}), do: "Listing deprecated features in use ..." + + def usage, do: "list_deprecated_features [--used] [ ...]" + + def usage_additional() do + [ + ["", "must be one of " <> Enum.join(Enum.sort(@info_keys), ", ")], + ["--used", "returns deprecated features in use"] + ] + end + + def usage_doc_guides() do + [ + DocGuide.feature_flags() + ] + end + + def help_section(), do: :feature_flags + + def description(), do: "Lists deprecated features" + + # + # Implementation + # + + defp filter_by_arg(ff_info, _) when is_tuple(ff_info) do + # tuple means unexpected data + ff_info + end + + defp filter_by_arg(ff_info, [_ | _] = args) when is_list(ff_info) do + symbol_args = InfoKeys.prepare_info_keys(args) + + Enum.map( + ff_info, + fn ff -> + symbol_args + |> Enum.filter(fn arg -> ff[arg] != nil end) + |> Enum.map(fn arg -> {arg, ff[arg]} end) + end + ) + end +end diff --git a/deps/rabbitmq_cli/test/ctl/list_deprecated_features_command_test.exs b/deps/rabbitmq_cli/test/ctl/list_deprecated_features_command_test.exs new file mode 100644 index 000000000000..d7bbf0f89529 --- /dev/null +++ b/deps/rabbitmq_cli/test/ctl/list_deprecated_features_command_test.exs @@ -0,0 +1,145 @@ +## 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) 2023 Broadcom. All Rights Reserved. The term “Broadcom” +## refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. + +defmodule ListDeprecatedFeaturesCommandTest do + use ExUnit.Case, async: false + import TestHelper + + @command RabbitMQ.CLI.Ctl.Commands.ListDeprecatedFeaturesCommand + + @df1 :df1_from_list_df_testsuite + @df2 :df2_from_list_df_testsuite + + setup_all do + RabbitMQ.CLI.Core.Distribution.start() + + # Define an arbitrary deprecated feature for the test. + node = get_rabbit_hostname() + + new_deprecated_features = %{ + @df1 => %{ + desc: ~c"My deprecated feature #1", + provided_by: :ListDeprecatedFeaturesCommandTest, + deprecation_phase: :permitted_by_default + }, + @df2 => %{ + desc: ~c"My deprecated feature #2", + provided_by: :ListDeprecatedFeaturesCommandTest, + deprecation_phase: :removed + } + } + + :ok = + :rabbit_misc.rpc_call( + node, + :rabbit_feature_flags, + :inject_test_feature_flags, + [new_deprecated_features] + ) + + name_result = [ + [{:name, @df1}], + [{:name, @df2}] + ] + + full_result = [ + [{:name, @df1}, {:deprecation_phase, :permitted_by_default}], + [{:name, @df2}, {:deprecation_phase, :removed}] + ] + + { + :ok, + name_result: name_result, full_result: full_result + } + end + + setup context do + { + :ok, + opts: %{node: get_rabbit_hostname(), timeout: context[:test_timeout], used: false} + } + end + + test "merge_defaults with no command, print just use the names" do + assert match?({["name", "deprecation_phase"], %{}}, @command.merge_defaults([], %{})) + end + + test "validate: return bad_info_key on a single bad arg", context do + assert @command.validate(["quack"], context[:opts]) == + {:validation_failure, {:bad_info_key, [:quack]}} + end + + test "validate: returns multiple bad args return a list of bad info key values", context do + result = @command.validate(["quack", "oink"], context[:opts]) + assert match?({:validation_failure, {:bad_info_key, _}}, result) + {_, {_, keys}} = result + assert :lists.sort(keys) == [:oink, :quack] + end + + test "validate: return bad_info_key on mix of good and bad args", context do + assert @command.validate(["quack", "name"], context[:opts]) == + {:validation_failure, {:bad_info_key, [:quack]}} + + assert @command.validate(["name", "oink"], context[:opts]) == + {:validation_failure, {:bad_info_key, [:oink]}} + + assert @command.validate(["name", "oink", "deprecation_phase"], context[:opts]) == + {:validation_failure, {:bad_info_key, [:oink]}} + end + + test "run: on a bad RabbitMQ node, return a badrpc" do + opts = %{node: :jake@thedog, timeout: 200, used: false} + assert match?({:badrpc, _}, @command.run(["name"], opts)) + end + + @tag test_timeout: :infinity + test "run: with the name tag, print just the names", context do + matches_found = @command.run(["name"], context[:opts]) + + assert Enum.all?(context[:name_result], fn feature_name -> + Enum.find(matches_found, fn found -> found == feature_name end) + end) + end + + @tag test_timeout: :infinity + test "run: with the name tag, print just the names for used features", context do + opts = %{node: get_rabbit_hostname(), timeout: context[:test_timeout], used: true} + matches_found = @command.run(["name"], opts) + + assert Enum.empty?(matches_found) + end + + @tag test_timeout: :infinity + test "run: duplicate args do not produce duplicate entries", context do + # checks to ensure that all expected deprecated features are in the results + matches_found = @command.run(["name", "name"], context[:opts]) + + assert Enum.all?(context[:name_result], fn feature_name -> + Enum.find(matches_found, fn found -> found == feature_name end) + end) + end + + @tag test_timeout: 30000 + test "run: sufficiently long timeouts don't interfere with results", context do + matches_found = @command.run(["name", "deprecation_phase"], context[:opts]) + + assert Enum.all?(context[:full_result], fn feature_name -> + Enum.find(matches_found, fn found -> found == feature_name end) + end) + end + + @tag test_timeout: 0, username: "guest" + test "run: timeout causes command to return a bad RPC", context do + assert @command.run(["name", "state"], context[:opts]) == + {:badrpc, :timeout} + end + + @tag test_timeout: :infinity + test "banner", context do + assert @command.banner([], context[:opts]) =~ ~r/Listing deprecated features \.\.\./ + end +end