How to handle empty list in Ansible loop

Viewed 1252

Context is modifying many DNS zone records across different servers, using community.windows.win_dns_record in a loop. Each DNS server has a var file in host_vars. The variable for the list of record changes is zone_records.

Sometimes there are no changes on a server, and I'd like to leave the zone_records variable undefined, but I am open to setting it to an empty list, or empty string.

Here an attempt is made to short-circuit the loop by when: zone_records is iterable

- name: Remove zone records
  community.windows.win_dns_record:
    computer_name: "{{ remove.server_name }}"
    zone: "{{ remove.zone }}"
    name: "{{ remove.name }}"
    state: "{{ remove.state }}"
    type: "{{ remove.type }}"
  loop:
    "{{ query('dict', zone_records) }}"
  when:
    - zone_records is iterable
    - remove.state == 'absent'
  loop_control:
    loop_var: remove

The above fails differently depending on whether zone_records is undefined, or a string instead of a list.
In the latter case, the error is:

Invalid data passed to 'loop', it requires a list

How should lists be handled in loops where sometimes there's no list or an empty list?

1 Answers

This will happen because the loop will be processed before the condition is even considered.

And also because a string is an iterable in Python, since it is, simply put, a list of characters.

iterable
An object capable of returning its members one at a time. Examples of iterables include all sequence types (such as list, str, and tuple) and some non-sequence types like dict, file objects, and objects of any classes you define with an __iter__() method or with a __getitem__() method that implements Sequence semantics.

Source: https://docs.python.org/3/glossary.html#term-iterable

You can go around this using a conditional expression in the loop definition itself:

- hosts: all
  gather_facts: no
      
  tasks:
    - debug: 
        msg: "{{ item }}"
      loop: "{{ foo if foo is iterable and foo is not string else [] }}"
      vars:
        foo: 'bar'

Would end in the host(s) being skipped (since an empty list is passed in the else of the conditional expression):

PLAY [all] *******************************************************************************************************

TASK [debug] *****************************************************************************************************

PLAY RECAP *******************************************************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   

While:

- hosts: all
  gather_facts: no
      
  tasks:
    - debug: 
        msg: "{{ item }}"
      loop: "{{ foo if foo is iterable and foo is not string else [] }}"
      vars:
        foo: 
          - 'bar'
          - 'baz'

Would end in the expected:

PLAY [all] *******************************************************************************************************

TASK [debug] *****************************************************************************************************
ok: [localhost] => (item=bar) => {
    "msg": "bar"
}
ok: [localhost] => (item=baz) => {
    "msg": "baz"
}

PLAY RECAP *******************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
Related