diff --git a/deps/rabbit/src/rabbit_definitions.erl b/deps/rabbit/src/rabbit_definitions.erl index ae1f30e4fa67..63f76e851b5c 100644 --- a/deps/rabbit/src/rabbit_definitions.erl +++ b/deps/rabbit/src/rabbit_definitions.erl @@ -1077,9 +1077,22 @@ runtime_parameter_definition(Param) -> <<"vhost">> => pget(vhost, Param), <<"component">> => pget(component, Param), <<"name">> => pget(name, Param), - <<"value">> => maps:from_list(pget(value, Param)) + <<"value">> => maybe_map(pget(value, Param)) }. +maybe_map(Value) when is_list(Value) -> + %% Not all definitions are maps. `federation-upstream-set` is + %% a list of maps, and it should be exported as it has been + %% imported + try + maps:from_list(Value) + catch + _:_ -> + Value + end; +maybe_map(Value) -> + Value. + list_global_runtime_parameters() -> [global_runtime_parameter_definition(P) || P <- rabbit_runtime_parameters:list_global(), not is_internal_parameter(P)]. diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/export_definitions_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/export_definitions_command.ex index c5299b927d62..cc21e2a0a46d 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/export_definitions_command.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/export_definitions_command.ex @@ -149,17 +149,9 @@ defmodule RabbitMQ.CLI.Ctl.Commands.ExportDefinitionsCommand do # defp serialise(raw_map, "json") do - # make sure all runtime parameter values are maps, otherwise - # they will end up being a list of pairs (a keyword list/proplist) - # in the resulting JSON document - map = - Map.update!(raw_map, :parameters, fn params -> - Enum.map(params, fn param -> - Map.update!(param, "value", &:rabbit_data_coercion.to_map/1) - end) - end) - - {:ok, json} = JSON.encode(map) + # rabbit_definitions already takes care of transforming all + # proplists into maps + {:ok, json} = JSON.encode(raw_map) json end diff --git a/deps/rabbitmq_federation/BUILD.bazel b/deps/rabbitmq_federation/BUILD.bazel index 569146dd5d8a..2ae8e23837a3 100644 --- a/deps/rabbitmq_federation/BUILD.bazel +++ b/deps/rabbitmq_federation/BUILD.bazel @@ -89,6 +89,11 @@ eunit( broker_for_integration_suites() +rabbitmq_integration_suite( + name = "definition_import_SUITE", + size = "small", +) + rabbitmq_integration_suite( name = "exchange_SUITE", size = "large", diff --git a/deps/rabbitmq_federation/app.bzl b/deps/rabbitmq_federation/app.bzl index 2f68ed025bc8..b183bea904a0 100644 --- a/deps/rabbitmq_federation/app.bzl +++ b/deps/rabbitmq_federation/app.bzl @@ -146,6 +146,15 @@ def all_srcs(name = "all_srcs"): ) def test_suite_beam_files(name = "test_suite_beam_files"): + erlang_bytecode( + name = "definition_import_SUITE_beam_files", + testonly = True, + srcs = ["test/definition_import_SUITE.erl"], + outs = ["test/definition_import_SUITE.beam"], + app_name = "rabbitmq_federation", + erlc_opts = "//:test_erlc_opts", + deps = ["//deps/rabbitmq_ct_helpers:erlang_app"], + ) erlang_bytecode( name = "exchange_SUITE_beam_files", testonly = True, diff --git a/deps/rabbitmq_federation/test/definition_import_SUITE.erl b/deps/rabbitmq_federation/test/definition_import_SUITE.erl new file mode 100644 index 000000000000..44f4afe3bb65 --- /dev/null +++ b/deps/rabbitmq_federation/test/definition_import_SUITE.erl @@ -0,0 +1,146 @@ +%% 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-2024 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. +%% + +-module(definition_import_SUITE). + +-include_lib("rabbitmq_ct_helpers/include/rabbit_assert.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-compile(export_all). + +all() -> + [ + {group, roundtrip} + ]. + +groups() -> + [ + {roundtrip, [], [ + export_import_round_trip + ]} + ]. + +%% ------------------------------------------------------------------- +%% Test suite setup/teardown. +%% ------------------------------------------------------------------- + +init_per_suite(Config) -> + rabbit_ct_helpers:log_environment(), + inets:start(), + Config. +end_per_suite(Config) -> + Config. + +init_per_group(Group, Config) -> + Config1 = rabbit_ct_helpers:set_config(Config, [ + {rmq_nodename_suffix, Group} + ]), + rabbit_ct_helpers:run_setup_steps(Config1, rabbit_ct_broker_helpers:setup_steps()). + +end_per_group(_, Config) -> + rabbit_ct_helpers:run_teardown_steps(Config, rabbit_ct_broker_helpers:teardown_steps()). + +init_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_started(Config, Testcase). + +end_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_finished(Config, Testcase). + +%% +%% Tests +%% + +export_import_round_trip(Config) -> + case rabbit_ct_helpers:is_mixed_versions() of + false -> + import_file_case(Config, "case1"), + Defs = export(Config), + import_raw(Config, rabbit_json:encode(Defs)); + _ -> + %% skip the test in mixed version mode + {skip, "Should not run in mixed version environments"} + end. + +%% +%% Implementation +%% + +import_file_case(Config, CaseName) -> + CasePath = filename:join([ + ?config(data_dir, Config), + CaseName ++ ".json" + ]), + rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, run_import_case, [CasePath]), + ok. + + +import_raw(Config, Body) -> + case rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_definitions, import_raw, [Body]) of + ok -> ok; + {error, E} -> + ct:pal("Import of JSON definitions ~tp failed: ~tp~n", [Body, E]), + ct:fail({expected_failure, Body, E}) + end. + +export(Config) -> + rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, run_export, []). + +run_export() -> + rabbit_definitions:all_definitions(). + +run_directory_import_case(Path, Expected) -> + ct:pal("Will load definitions from files under ~tp~n", [Path]), + Result = rabbit_definitions:maybe_load_definitions_from(true, Path), + case Expected of + ok -> + ok = Result; + error -> + ?assertMatch({error, {failed_to_import_definitions, _, _}}, Result) + end. + +run_import_case(Path) -> + {ok, Body} = file:read_file(Path), + ct:pal("Successfully loaded a definition to import from ~tp~n", [Path]), + case rabbit_definitions:import_raw(Body) of + ok -> ok; + {error, E} -> + ct:pal("Import case ~tp failed: ~tp~n", [Path, E]), + ct:fail({expected_failure, Path, E}) + end. + +run_invalid_import_case(Path) -> + {ok, Body} = file:read_file(Path), + ct:pal("Successfully loaded a definition file at ~tp~n", [Path]), + case rabbit_definitions:import_raw(Body) of + ok -> + ct:pal("Expected import case ~tp to fail~n", [Path]), + ct:fail({expected_failure, Path}); + {error, _E} -> ok + end. + +run_invalid_import_case_if_unchanged(Path) -> + Mod = rabbit_definitions_import_local_filesystem, + ct:pal("Successfully loaded a definition to import from ~tp~n", [Path]), + case rabbit_definitions:maybe_load_definitions_from_local_filesystem_if_unchanged(Mod, false, Path) of + ok -> + ct:pal("Expected import case ~tp to fail~n", [Path]), + ct:fail({expected_failure, Path}); + {error, _E} -> ok + end. + +queue_lookup(Config, VHost, Name) -> + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_amqqueue, lookup, [rabbit_misc:r(VHost, queue, Name)]). + +vhost_lookup(Config, VHost) -> + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_vhost, lookup, [VHost]). + +user_lookup(Config, User) -> + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, lookup_user, [User]). + +delete_vhost(Config, VHost) -> + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_vhost, delete, [VHost, <<"CT tests">>]). diff --git a/deps/rabbitmq_federation/test/definition_import_SUITE_data/case1.json b/deps/rabbitmq_federation/test/definition_import_SUITE_data/case1.json new file mode 100644 index 000000000000..e549e4fd6c1d --- /dev/null +++ b/deps/rabbitmq_federation/test/definition_import_SUITE_data/case1.json @@ -0,0 +1,52 @@ +{ + "permissions": [ + { + "configure": ".*", + "read": ".*", + "user": "guest", + "vhost": "/", + "write": ".*" + } + ], + "bindings": [], + "queues": [], + "parameters": [ + { + "component": "federation-upstream-set", + "name": "location-1", + "value": [ + { + "upstream":"up-1" + }, + { + "upstream":"up-2" + } + ], + "vhost":"/"}], + "policies": [], + "rabbitmq_version": "3.13.0+376.g1bc0d89.dirty", + "users": [ + { + "hashing_algorithm": "rabbit_password_hashing_sha256", + "limits": {}, + "name": "guest", + "password_hash": "jTcCKuOmGJeeRQ/K1LG5sdZLcdnEnqv8wcrP2n68R7nMuqy2", + "tags": ["administrator"] + } + ], + "rabbit_version": "3.13.0+376.g1bc0d89.dirty", + "exchanges": [], + "topic_permissions": [], + "vhosts": [ + { + "limits": [], + "metadata": + { + "description": "Default virtual host", + "tags": [] + }, + "name":"/" + } + ], + "global_parameters": [] +}