Description
SUMMARY
When a binding is created, it will calculate the properties_key
based on the routing_key
and arguments
:
# pseudo code
if len(arguments) == 0 and routing_key == "":
properties_key = "~"
else if len(arguments) == 0 and routing_key != "":
properties_key = routing_key
else if len(arguments) > 0:
properties_key = f"{routing_key}~{hash(arguments)}"
The actual
hash()
algorithm will be mentioned later
When rabbitmq_binding checks if the resource exists, it invokes the following request:
# https://github.com/ansible-collections/community.rabbitmq/blob/146b006de99112e1abb5c2be0c4f2bcc8d6c3a5f/plugins/modules/rabbitmq_binding.py#L114-L123
self.props = urllib_parse.quote(self.routing_key) if self.routing_key != '' else '~'
self.base_url = '{0}://{1}:{2}/api/bindings'.format(self.login_protocol,
self.login_host,
self.login_port)
self.url = '{0}/{1}/e/{2}/{3}/{4}/{5}'.format(self.base_url,
urllib_parse.quote(self.vhost, safe=''),
urllib_parse.quote(self.name, safe=''),
self.destination_type,
urllib_parse.quote(self.destination, safe=''),
self.props)
The logic does not consider arguments
. As a result, bindings with arguments will not have idempotency and will introduce some incorrect behavior.
The check uses the HTTP API /api/bindings/vhost/e/exchange/q/queue/props, which describes as below:
An individual binding between an exchange and a queue. The props part of the URI is a "name" for the binding composed of its routing key and a hash of its arguments. props is the field named "properties_key" from a bindings listing response.
The following snippet describes the actual hash algorithm:
%% https://github.com/rabbitmq/rabbitmq-server/blob/70047f52453c37d8f3fa185807c8f574dd95ef71/deps/rabbitmq_management_agent/src/rabbit_mgmt_format.erl#L412-L424
binding(#binding{source = S,
key = Key,
destination = D,
args = Args}) ->
format(
[{source, S},
{destination, D#resource.name},
{destination_type, D#resource.kind},
{routing_key, Key},
{arguments, Args},
{properties_key, pack_binding_props(Key, Args)}],
{fun format_binding/1, false}).
%% https://github.com/rabbitmq/rabbitmq-server/blob/70047f52453c37d8f3fa185807c8f574dd95ef71/deps/rabbitmq_management_agent/src/rabbit_mgmt_format.erl#L336-L342
pack_binding_props(<<"">>, []) ->
<<"~">>;
pack_binding_props(Key, []) ->
list_to_binary(quote_binding(Key));
pack_binding_props(Key, Args) ->
ArgsEnc = args_hash(Args),
list_to_binary(quote_binding(Key) ++ "~" ++ quote_binding(ArgsEnc)).
%% https://github.com/rabbitmq/rabbitmq-server/blob/70047f52453c37d8f3fa185807c8f574dd95ef71/deps/rabbitmq_management_agent/src/rabbit_mgmt_format.erl#L605-L606
args_hash(Args) ->
list_to_binary(rabbit_misc:base64url(<<(erlang:phash2(Args, 1 bsl 32)):32>>)).
%% https://github.com/rabbitmq/rabbitmq-server/blob/70047f52453c37d8f3fa185807c8f574dd95ef71/deps/rabbit_common/src/rabbit_misc.erl#L1120-L1125
base64url(In) ->
lists:reverse(lists:foldl(fun ($\+, Acc) -> [$\- | Acc];
($\/, Acc) -> [$\_ | Acc];
($\=, Acc) -> Acc;
(Chr, Acc) -> [Chr | Acc]
end, [], base64:encode_to_string(In))).
I do not write Erlang, let me know if I am grabbing the wrong blocks
Suggested fixes:
- Instead of using
self.http_check_states.get(self.api_result.status_code, False)
, retrieve all bindings via/api/bindings/vhost/e/exchange/q/queue
and check if the resource exists, - Use the same method to create the
properties_key
and place it in URL when invoking/api/bindings/vhost/e/exchange/q/queue/props
ISSUE TYPE
- Bug Report
COMPONENT NAME
community.rabbitmq.rabbitmq_binding
ANSIBLE VERSION
ansible [core 2.15.3]
config file = /var/user/ansible/ansible.cfg
configured module search path = ['/home/jeffrey/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /home/jeffrey/.local/lib/python3.10/site-packages/ansible
ansible collection location = /home/jeffrey/.ansible/collections
executable location = /home/jeffrey/.local/bin/ansible
python version = 3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0] (/usr/bin/python3)
jinja version = 3.0.3
libyaml = True
COLLECTION VERSION
# /home/jeffrey/.ansible/collections/ansible_collections
Collection Version
------------------ -------
community.rabbitmq 1.2.3
CONFIGURATION
Omitted
OS / ENVIRONMENT
OS: Ubuntu 22.04.3 LTS
Kernel: 5.15.0-76-generic
RabbitMQ: 3.9.13-1ubuntu0.22.04.1 (current latest release with Ubuntu 22.04)
STEPS TO REPRODUCE
Run the following tasks at least once:
- name: Bind queue to exchange rk/ arg/
community.rabbitmq.rabbitmq_binding:
name: xexchange
destination: xqueue
type: queue
routing_key: ""
- name: Bind queue to exchange rk/aaa arg/AAA
community.rabbitmq.rabbitmq_binding:
name: xexchange
destination: xqueue
type: queue
routing_key: "aaa"
arguments:
type: "AAA"
- name: Bind queue to exchange rk/bbb arg/
community.rabbitmq.rabbitmq_binding:
name: xexchange
destination: xqueue
type: queue
routing_key: "bbb"
- name: Bind queue to exchange rk/# arg/CCC
community.rabbitmq.rabbitmq_binding:
name: xexchange
destination: xqueue
type: queue
arguments:
type: "CCC"
- name: Bind queue to exchange rk/ arg/DDD
community.rabbitmq.rabbitmq_binding:
name: xexchange
destination: xqueue
type: queue
routing_key: ""
arguments:
type: "DDD"
EXPECTED RESULTS
After the first run, all consecutive runs should report ok: [...]
instead of changed: [...]
ACTUAL RESULTS
TASK [Bind queue to exchange rk/ arg/] *************************************************
ok: [lab1]
TASK [Bind queue to exchange rk/aaa arg/AAA] *******************************************
changed: [lab1]
TASK [Bind queue to exchange rk/bbb arg/] **********************************************
ok: [lab1]
TASK [Bind queue to exchange rk/# arg/CCC] *********************************************
changed: [lab1]
TASK [Bind queue to exchange rk/ arg/DDD] **********************************************
ok: [lab1]
case | routing_key | arguments | idempotency | properties_key |
---|---|---|---|---|
1 | ok | finds "~", and it is "~" | ||
2 | aaa | type:AAA | changed | finds "aaa", but actually "aaa~xxxx" |
3 | bbb | ok | finds "bbb", and it is "bbb" | |
4 | # | type:CCC | changed | finds "#", and actually "#~xxxx", similar to case 2 |
5 | type:DDD | ok (wrong!) | finds "~", found "~", but actually matches the binding created by case 1 |
Invoke the HTTP API and see the properties_key:
curl -u guest -s http://localhost:15672/api/bindings/%2F/e/xexchange/q/xqueue | jq
[
{
"source": "xexchange",
"vhost": "/",
"destination": "xqueue",
"destination_type": "queue",
"routing_key": "",
"arguments": {},
"properties_key": "~"
},
{
"source": "xexchange",
"vhost": "/",
"destination": "xqueue",
"destination_type": "queue",
"routing_key": "",
"arguments": {
"type": "DDD"
},
"properties_key": "~7V4gwQ"
},
{
"source": "xexchange",
"vhost": "/",
"destination": "xqueue",
"destination_type": "queue",
"routing_key": "#",
"arguments": {
"type": "CCC"
},
"properties_key": "%23~fF974A"
},
{
"source": "xexchange",
"vhost": "/",
"destination": "xqueue",
"destination_type": "queue",
"routing_key": "aaa",
"arguments": {
"type": "AAA"
},
"properties_key": "aaa~yG2vtw"
},
{
"source": "xexchange",
"vhost": "/",
"destination": "xqueue",
"destination_type": "queue",
"routing_key": "bbb",
"arguments": {},
"properties_key": "bbb"
}
]