Make query with errors='ignore' return a blank list (#57038)

The jinja2 query() function (or lookup with wantslist=True, which is
the same thing) should always return a list.

However, if you combine a query with errors='ignore' and take the
error path, the current code returns a None value.  This is important
in a case such as

 - name: Conditional include file
   import_tasks: '{{ item }}'
   vars:
     params:
       files:
         - path/file1.yaml
         - path/file2.yaml
   loop: "{{ q('first_found', params, errors='ignore') }}"

If neither file1.yaml or file2.yaml exist, this should do nothing by
returning an empty list to the loop.  Currently if you run the above
task you'll get a rather unhelpful:

 Invalid data passed to 'loop', it requires a list, got this instead: .

This change ensures that when a query ignores an error, it returns a
empty list.  The errors='ignore' case is tested in several variants
with first_found.  The extant (but deprecated) "skip: True" for
first_found doesn't seem to be explicitly tested; a test is added here
to avoid regressions before removal in 2.12.

This fixes a regression you'll hit if you follow the suggestion in the
deprecation message included with
e17a2b502d to use errors=ignore over
"skip: True" for first_found.  This change adds an example that points
out the query/lookup difference and also fixes the error message to
not mention the now deprecated "skip: True".

Closes #56775
This commit is contained in:
Ian Wienand 2019-05-30 03:23:03 +10:00 committed by Brian Coca
parent b7f4e2a190
commit 39945b8570
3 changed files with 69 additions and 2 deletions

View file

@ -44,6 +44,17 @@ EXAMPLES = """
- "bar.txt" # will be looked in files/ dir relative to role and/or play
- "/path/to/biz.txt"
- name: |
include tasks only if files exist. Note the use of query() to return
a blank list for the loop if no files are found.
import_tasks: '{{ item }}'
vars:
params:
files:
- path/tasks.yaml
- path/other_tasks.yaml
loop: "{{ q('first_found', params, errors='ignore') }}"
- name: |
copy first existing file found to /some/file,
looking in relative directories from where the task is defined and
@ -166,5 +177,5 @@ class LookupModule(LookupBase):
return [path]
if skip:
return []
raise AnsibleLookupError("No file was found when using first_found. Use the 'skip: true' option to allow this task to be skipped if no "
raise AnsibleLookupError("No file was found when using first_found. Use errors='ignore' to allow this task to be skipped if no "
"files are found")

View file

@ -706,7 +706,7 @@ class Templar:
display.display(msg, log_only=True)
else:
raise AnsibleError(to_native(msg))
ran = None
ran = [] if wantlist else None
if ran and not allow_unsafe:
if wantlist:

View file

@ -270,3 +270,59 @@
- "b__ == 'flattened'"
- "c__ == 'flattened'"
- "d__ == 'flattened'"
# q(FIRST_FOUND)
- name: test q(first_found) with no files produces empty list
set_fact:
first_found_var: "{{ q('first_found', params, errors='ignore') }}"
vars:
params:
files: "not_a_file.yaml"
- name: verify q(first_found) result
assert:
that:
- "first_found_var == []"
- name: test lookup(first_found) with no files produces empty string
set_fact:
first_found_var: "{{ lookup('first_found', params, errors='ignore') }}"
vars:
params:
files: "not_a_file.yaml"
- name: verify lookup(first_found) result
assert:
that:
- "first_found_var == ''"
# NOTE: skip: True deprecated e17a2b502d6601be53c60d7ba1c627df419460c9, remove 2.12
- name: test first_found with no matches and skip=True does nothing
set_fact: "this_not_set={{ item }}"
vars:
params:
files:
- not/a/file.yaml
- another/non/file.yaml
skip: True
loop: "{{ q('first_found', params) }}"
- name: verify skip
assert:
that:
- "this_not_set is not defined"
- name: test first_found with no matches and errors='ignore' skips in a loop
set_fact: "this_not_set={{ item }}"
vars:
params:
files:
- not/a/file.yaml
- another/non/file.yaml
loop: "{{ query('first_found', params, errors='ignore') }}"
- name: verify errors=ignore
assert:
that:
- "this_not_set is not defined"