Checking that keys nested in a dictionary are unique using JMESPath (or not)

I have a PHP role that defines the versions of PHP to install and the FPM pools to configure and enable that looks something like this:

php_versions:
  8.1:
    state: present
    pools:
      admin:
        state: present
        listen: /run/php/admin_php-fpm.sock
      www:
        state: present
        listen: /run/php/php8.1-fpm.sock
  8.2:
    state: present
    pools:
      www:
        state: present
        listen: /run/php/php8.2-fpm.sock

And for monitoring purposes I have realised that the PHP-FPM pool names need to be unique, there can’t be a www pool for more than one version of PHP as there is currently.

What would be the best way to check that the pools keys are unique when the PHP version state is present and the pool state is present across the PHP versions?

This role is going to have to delete some www pools using a config like this:

php_versions:
  8.1:
    state: present
    pools:
      admin:
        state: present
        listen: /run/php/admin_php-fpm.sock
      www:
        state: absent
      www81:
        state: present
        listen: /run/php/php8.1-fpm.sock
  8.2:
    state: present
    pools:
      www:
        state: absent
      www82:
        state: present
        listen: /run/php/php8.2-fpm.sock

I have got as far as a JMESPath query to generate lists of the pool names (using the first version of php_versions above):

- name: Debug pool names
  ansible.builtin.debug:
    msg: "{{ php_versions | community.general.json_query('*.pools[].keys(@)') }}"
  when: php_versions is defined

This returns:

ok: [php] => {
    "msg": [
        [
            "www"
        ],
        [
            "admin",
            "www"
        ]
    ]
}

I expect I could put something together using loops and include_tasks but I was wondering if there would be a better way someone could suggest…

BTW I added the community tag here assuming that it was a reference to the community.general collection but perhaps it isn’t?

Indeed not. We’re still working out how to get the best of tags for the collections, given there are so many - @felixfontein how would you handle this, do you think? Is a community.general tag warranted?

1 Like

There is going to be 10 ways of doing it, I would suggest a new filter though, based on the original code
Guide on creating a new filter

It is possible to do this in multiple tasks, loops, and various things,

When I tried to run your play I got the following out though

 {
    "msg": [
        [
            "admin",
            "www",
            "www81"
        ],
        [
            "www",
            "www82"
        ]
    ]
}

in addition I believe the community.general tag would likely be needed as community or general itself are very generic, but in our world, community.general is generally understood

2 Likes

Thanks for loking at this @sean_sullivan , the reason you got a different output is because you used a different input, the output I posted was for the first example of php_versions I provided and your output was for the second, sorry my first post wasn’t clear, I edited it to add the second example when I realised that it’s fine to have duplicated pools and versions that are absent — only present ones need to be checked for duplicates.

The JMESPath expression can be improved to only match versions of PHP that are set to be present (using the second version of php_versions above):

- name: Debug pool names
  ansible.builtin.debug:
    msg: "{{ php_versions | community.general.json_query('*|[?state==`present`].pools') }}"

This result in:

[
  {
    "admin": {
      "state": "present",
      "listen": "/run/php/admin_php-fpm.sock"
    },
    "www": {
      "state": "absent"
    },
    "www81": {
      "state": "present",
      "listen": "/run/php/php8.1-fpm.sock"
    }
  },
  {
    "www": {
      "state": "absent"
    },
    "www82": {
      "state": "present",
      "listen": "/run/php/php8.2-fpm.sock"
    }
  }
]

However I can’t work out how to extract the keys for the pools that are present, so looping through this list and including another file:

    - name: Loop through the PHP-FPM versions
      ansible.builtin.include_tasks: fpm_pool_checks.yml
      loop: "{{ php_versions | community.general.json_query('*|[?state==`present`].pools') }}"
      loop_control:
        loop_var: php_pool

With fpm_pool_checks.yml containing another loop that sets a fact to create a list of pool names:

- name: Set a fact for the pools that are present
  ansible.builtin.set_fact:
    php_pool_names: "{{ php_pool_names + [ php_pool_vars.key ] }}"
  when: php_pool_vars.value.state == "present"
  loop: "{{ php_pool | dict2items }}"
  loop_control:
    loop_var: php_pool_vars

Then in the first file a check can be made for duplicates:

- name: Check that no PHP-FPM pool names are duplicated
  ansible.builtin.assert:
    that:
      - php_pool_names | length == php_pool_names | unique | length
    fail_msg: |-
      PHP-FPM pool names should be unique
      {% for php_pool_n in php_pool_names %} {{php_pool_n }}{% endfor %}

If there are duplicates the role fails:

TASK [php : Check that no PHP-FPM pool names are duplicated] **************************************************************************
fatal: [php]: FAILED! => changed=false 
  assertion: php_pool_names | length == php_pool_names | unique | length
  evaluated_to: false
  msg: 'PHP-FPM pool names should be unique: www81 www admin www'

Perhaps this is one of the least elegant of the “10 ways of doing it” … if anyone can suggest a solution that doesn’t need a couple of loops and an include_tasks I’d be interested to hear it :slight_smile: .

With hindsight I’d have used lists for the PHP versions and pools rather than a dictionary as they are easier to manipulate, at the time I used a dictionary because they look nicer…