I struggle on a regular basis with data manipulation in Ansible. I'm not very familiar with Python and dict objects. I found an example that sums up a lot of my misunderstandings.
I would like to verify a list of certificates. In found an example for a single domain in the documentation, I'm just trying to loop over several domain names.
Certs are stored in a folder:
certs/
├── domain.com
│   ├── domain.com.pem
│   └── domain.com.key
└── domain.org
    ├── domain.org.key
    └── domain.org.pem
My playbook is as follow:
---
- name: "check certs"
  hosts: localhost
  gather_facts: no
  vars:
    domain_names:
      - domain.com
      - domain.org
    certs_folder: certs
  tasks: 
    - name: Get certificate information
      community.crypto.x509_certificate_info:
        path: "{{ certs_folder }}/{{ item }}/{{ item }}.pem"
        # for valid_at, invalid_at and valid_in
      register: result_certs
      loop: "{{ domain_names }}"
      failed_when: 0
    - name: Get private key information
      community.crypto.openssl_privatekey_info:
        path: "{{ certs_folder }}/{{ item }}/{{ item }}.key"
      register: result_privatekey
      loop: "{{ domain_names }}"
      failed_when: 0
    - name: Check cert and key match << DOES NOT WORK >>>
      assert: 
        that:
          - result_certs[ {{ item }} ].public_key == result_privatekey[ {{ item }} ].public_key
          # - ... other checks ...
          - not result[ {{ item }} ].expired
      loop: "{{ domain_names }}"
So I get two variables result_certs and result_privatekey, each has a element result which is , if  I understand correctly, an array of dicts:
"result_certs": {
        "changed": false,
        "msg": "All items completed",
        "results": [
            {
                "expired": false,
                "item": "domain.org",
                "public_key": "<<PUBLIC KEY>>"
            },
            {
                "expired": false,
                "item": "domain.com",
                "public_key": "<<PUBLIC KEY>>"
            }
        ],
        "skipped": false
    }
"result_privatekey": {
    "changed": false,
    "msg": "All items completed",
    "results": [
    {
        "item": "domain.org",
        "public_key": "<< PUBLIC KEY >>"
    },
    {
        "item": "domain.com",
        "public_key": "<< PUBLIC KEY >>"
    }
    ],
    "skipped": false
}
How can I refer to each of the dicts elements like result_privatekey.results[the dict where item ='domain.org'].public_key in the assert task?
I feel like I'm missing something, or a documentation page to make it clear to me. I noticed that I particularly struggle with arrays of dicts, and I run into those objects quite often...
I found those resources useful, but not sufficient to get this job done:
EDIT:
map and selectattr are the filters required to solve this problem, although the documentation (including the official ansible doc) is not that clear to me... This is very useful to get many tutorial examples on those two filters if one is struggling as I do.
Given the simplified data for testing
  result_certs:
    changed: false
    msg: All items completed
    results:
      - expired: false
        item: domain.org
        public_key: <<PUBLIC KEY domain.org>>
      - expired: false
        item: domain.com
        public_key: <<PUBLIC KEY domain.com>>
    skipped: false
  result_privatekey:
    changed: false
    msg: All items completed
    results:
      - item: domain.org
        public_key: <<PUBLIC KEY domain.org>>
      - item: domain.com
        public_key: <<PUBLIC KEY domain.com>>
    skipped: false
Declare the list of the domains
  domains: "{{ result_certs.results|
               map(attribute='item')|list }}"
gives
  domains:
  - domain.org
  - domain.com
Q: "How can I refer to each dictionary element?"
A: select the item(s) and map the attribute
    - debug:
        var: pk
      loop: "{{ domains }}"
      vars:
        pk: "{{ result_privatekey.results|
                selectattr('item', '==', item)|
                map(attribute='public_key')|list }}"
gives
TASK [debug] *********************************************************************************
ok: [localhost] => (item=domain.org) => 
  ansible_loop_var: item
  item: domain.org
  pk:
  - <<PUBLIC KEY domain.org>>
ok: [localhost] => (item=domain.com) => 
  ansible_loop_var: item
  item: domain.com
  pk:
  - <<PUBLIC KEY domain.com>>
In the same way, you can compare the keys in the loop
    - debug:
        msg: "{{ pk1 }} == {{ pk2 }}: {{ pk1 == pk2 }}"
      loop: "{{ domains }}"
      vars:
        pk1: "{{ result_privatekey.results|
                 selectattr('item', '==', item)|
                 map(attribute='public_key')|first }}"
        pk2: "{{ result_certs.results|
                 selectattr('item', '==', item)|
                 map(attribute='public_key')|first }}"
gives
TASK [debug] *********************************************************************************
ok: [localhost] => (item=domain.org) => 
  msg: '<<PUBLIC KEY domain.org>> == <<PUBLIC KEY domain.org>>: True'
ok: [localhost] => (item=domain.com) => 
  msg: '<<PUBLIC KEY domain.com>> == <<PUBLIC KEY domain.com>>: True'
Q: "Extract and manipulate dict data to check certificates."
A: There are many options:
  pkeys: "{{ result_certs.results|
             zip(result_privatekey.results)|
             map('map', attribute='public_key')|
             map('unique')|flatten }}"
gives
  pkeys:
  - <<PUBLIC KEY domain.org>>
  - <<PUBLIC KEY domain.com>>
To find the redundant keys compare the lists pkeys and domains. Compare the lengths to briefly find out if there are any
  pkeys|length == domains|length
To find the expired domains declare variables
  expired: "{{ result_certs.results|
               map(attribute='expired')|list }}"
  expired_domains: "{{ result_certs.results|
                       selectattr('expired')|
                       map(attribute='item')|list }}"
give
  expired:
  - false
  - false
  expired_domains: []
Then the assert task should look like
    - assert:
        that:
          - expired is not any
          - pkeys|length == domains|length
Example of a complete playbook for testing
- hosts: localhost
  vars:
    result_certs:
      changed: false
      msg: All items completed
      results:
      - expired: false
        item: domain.org
        public_key: <<PUBLIC KEY domain.org>>
      - expired: false
        item: domain.com
        public_key: <<PUBLIC KEY domain.com>>
      skipped: false
    result_privatekey:
      changed: false
      msg: All items completed
      results:
      - item: domain.org
        public_key: <<PUBLIC KEY domain.org>>
      - item: domain.com
        public_key: <<PUBLIC KEY domain.com>>
      skipped: false
    domains: "{{ result_certs.results|
                 map(attribute='item')|list }}"
    pkeys: "{{ result_certs.results|
               zip(result_privatekey.results)|
               map('map', attribute='public_key')|
               map('unique')|flatten }}"
    expired: "{{ result_certs.results|
                 map(attribute='expired')|list }}"
    expired_domains: "{{ result_certs.results|
                         selectattr('expired')|
                         map(attribute='item')|list }}"
  tasks:
    - debug:
        var: domains
    - debug:
        var: pkeys
    # How can I refer to each of the dicts elements?
    - debug:
        var: pk
      loop: "{{ domains }}"
      vars:
        pk: "{{ result_privatekey.results|
                selectattr('item', '==', item)|
                map(attribute='public_key')|list }}"
    # How can I compare private keys?
    - debug:
        msg: "{{ pk1 }} == {{ pk2 }}: {{ pk1 == pk2 }}"
      loop: "{{ domains }}"
      vars:
        pk1: "{{ result_privatekey.results|
                 selectattr('item', '==', item)|
                 map(attribute='public_key')|first }}"
        pk2: "{{ result_certs.results|
                 selectattr('item', '==', item)|
                 map(attribute='public_key')|first }}"
    - debug:
        var: expired
    - debug:
        var: expired_domains
    - assert:
        that:
          - expired is not any
          - pkeys|length == domains|length
gives
PLAY [localhost] *****************************************************************************
TASK [debug] *********************************************************************************
ok: [localhost] => 
  domains:
  - domain.org
  - domain.com
TASK [debug] *********************************************************************************
ok: [localhost] => 
  pkeys:
  - <<PUBLIC KEY domain.org>>
  - <<PUBLIC KEY domain.com>>
TASK [debug] *********************************************************************************
ok: [localhost] => (item=domain.org) => 
  ansible_loop_var: item
  item: domain.org
  pk:
  - <<PUBLIC KEY domain.org>>
ok: [localhost] => (item=domain.com) => 
  ansible_loop_var: item
  item: domain.com
  pk:
  - <<PUBLIC KEY domain.com>>
TASK [debug] *********************************************************************************
ok: [localhost] => (item=domain.org) => 
  msg: '<<PUBLIC KEY domain.org>> == <<PUBLIC KEY domain.org>>: True'
ok: [localhost] => (item=domain.com) => 
  msg: '<<PUBLIC KEY domain.com>> == <<PUBLIC KEY domain.com>>: True'
TASK [debug] *********************************************************************************
ok: [localhost] => 
  expired:
  - false
  - false
TASK [debug] *********************************************************************************
ok: [localhost] => 
  expired_domains: []
TASK [assert] ********************************************************************************
ok: [localhost] => changed=false 
  msg: All assertions passed
PLAY RECAP ***********************************************************************************
localhost: ok=7    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
  l1: "{{ result_certs.results|json_query('[].{item: item,
                                               expired: expired,
                                               pk: [public_key]}') }}"
  l2: "{{ result_privatekey.results|json_query('[].{item: item,
                                                    pk: [public_key]}') }}"
  lm: "{{ [l1, l2]|community.general.lists_mergeby('item', 
                                                    list_merge='append_rp') }}"
gives
  lm:
  - expired: false
    item: domain.com
    pk:
    - <<PUBLIC KEY domain.com>>
  - expired: false
    item: domain.org
    pk:
    - <<PUBLIC KEY domain.org>>
Use this structure to compare any attributes. For example, declare
  exprd: "{{ lm|map(attribute='expired')|list }}"
  pkeys: "{{ lm|map(attribute='pk')|
                map('length')|sum }}"
gives
  exprd:
  - false
  - false
  pkeys: '2'
Then, use it in the conditions
    - assert:
        that:
          - exprd is not any
          - pkeys|int == lm|length
Example of a complete playbook for testing
- hosts: localhost
  vars:
    result_certs:
      changed: false
      msg: All items completed
      results:
      - expired: false
        item: domain.org
        public_key: <<PUBLIC KEY domain.org>>
      - expired: false
        item: domain.com
        public_key: <<PUBLIC KEY domain.com>>
      skipped: false
    result_privatekey:
      changed: false
      msg: All items completed
      results:
      - item: domain.org
        public_key: <<PUBLIC KEY domain.org>>
      - item: domain.com
        public_key: <<PUBLIC KEY domain.com>>
      skipped: false
    l1: "{{ result_certs.results|json_query('[].{item: item,
                                                 expired: expired,
                                                 pk: [public_key]}') }}"
    l2: "{{ result_privatekey.results|json_query('[].{item: item,
                                                      pk: [public_key]}') }}"
    lm: "{{ [l1, l2]|
            community.general.lists_mergeby('item',
                                            list_merge='append_rp') }}"
    exprd: "{{ lm|map(attribute='expired')|list }}"
    pkeys: "{{ lm|map(attribute='pk')|
                  map('length')|sum }}"
  tasks:
    - debug:
        var: l1
    - debug:
        var: l2
    - debug:
        var: lm
    - debug:
        var: exprd
    - debug:
        var: pkeys
    - assert:
        that:
          - exprd is not any
          - pkeys|int == lm|length
  domain_exprd: "{{ lm|items2dict(key_name='item', value_name='expired') }}"
  domain_pkeys: "{{ lm|items2dict(key_name='item', value_name='pk') }}"
gives
  domain_exprd:
    domain.com: false
    domain.org: false
  domain_pkeys:
    domain.com:
    - <<PUBLIC KEY domain.com>>
    domain.org:
    - <<PUBLIC KEY domain.org>>
                        From what you are showing — but you might have more conditions that needs it — I wouldn't loop on the domain_names in your assertion task, I would rather loop on result_certs.
From there on, you can select the corresponding private key thanks to the selectattr filter.
So, your assertion would become:
- assert: 
    that:
      - >-
          item.public_key == (
            result_privatekey.results 
              | selectattr('item', '==', item.item) 
              | first
          ).public_key
      # - ... other checks ...
      - not item.expired
  loop: "{{ result_certs.results }}"
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With