Skip to content

rabbitmq_binding breaks idempotency if arguments is not empty #160

Closed
@oneoneonepig

Description

@oneoneonepig
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:

  1. 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,
  2. 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"
  }
]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions