Generally speaking: is there a way to break out of an Ansible loop and exit the current task?
Specifically: I am downloading JARs from an Artifactory instance using a loop, I want to check if the file exists first and, if the file does not exist, then log the error and exit.
My playbook like this:
- name: Download jars from artifactory
get_url:
url: "https://example.org/{{ item }}"
dest: /tmp
loop:
- a.jar
- b.jar
- c.jar
How to implement this?
This is not possible, for the moment being, but it won't be long until you can achieve this.
The current way to go is to skip the element(s) subsequent to the desired condition.
Or Ansible core version 2.18.
Please note: this version is a pre-release, for now, so, this is a sneak peak of what you will be able to do in the upcoming version.
As you can read from this feature request in Ansible's issue tracker:
There are cases where a task should be looped until a condition is met, like a
break
in a for loop. This can somewhat be achieved with awhen
statement, but it's overly complex and not apparent what the goal is, and can be made overly complicated when there is a need for thewhen
statement to provide different functionality to determine if the loop item should run.
Source: Ansible's issues tracker #83442
In the linked pull request, we can read in the release notes:
loop_control
- add abreak_when
option to break out of a task loop early based on Jinja2 expressions
Source: Pull request #62151 in Ansible's git repository
The syntax to use it is based on an attribute of loop_control
: break_when
.
The break_when
condition is evaluated right after the item have been processed.
- name: Count until 3, then break
ansible.builtin.debug:
msg: "{{ item }}"
loop:
- 1
- 2
- 3
- 4
- 5
loop_control:
break_when: item >= 3
Which will yield:
TASK [Count until 3, then break] **********************************
ok: [localhost] => (item=1) =>
msg: 1
ok: [localhost] => (item=2) =>
msg: 2
ok: [localhost] => (item=3) =>
msg: 3
This output has been generated with a pre-release version of Ansible (11.0.0a1), so be careful that the actual release 11.0.0 might still differ.
Or Ansible core version 2.18.
For the moment being, you would just skip the consecutive items based on a when
condition:
- name: Count until 3, then skip all further items
ansible.builtin.debug:
msg: "{{ item }}"
loop:
- 1
- 2
- 3
- 4
- 5
when: item <= 3
Which would yield:
TASK [Count until 3, then skip all further items] *****************
ok: [localhost] => (item=1) =>
msg: 1
ok: [localhost] => (item=2) =>
msg: 2
ok: [localhost] => (item=3) =>
msg: 3
skipping: [localhost] => (item=4)
skipping: [localhost] => (item=5)
Note: We are going to display the behaviour with the help of a command
task, but the idea can be applied to any module that implements properly a failed
state.
Or Ansible core version 2.18.
Since break_when
will apply on the currently looped item after it as been processed, you can register
the task and apply a failed
test on the registered variable.
You will still have a failing task, though, so, if you want to process further, you will want to use a rescue
block.
Given:
- block:
- name: Run commands until the first failure
ansible.builtin.command: "{{ item }}"
loop:
- 'true'
- 'false'
- 'ls'
loop_control:
break_when: _command is failed
register: _command
- name: Reporting successful processing
ansible.builtin.debug:
msg: All commands were processed
rescue:
- name: Reporting failure in processing
ansible.builtin.debug:
msg: >-
We processed commands until `{{ _command.results[-1].item }}`
You will get:
TASK [Run commands until the first failure] ***********************
changed: [localhost] => (item=true)
failed: [localhost] (item=false) => changed=true
ansible_loop_var: item
cmd:
- 'false'
delta: '0:00:00.002747'
end: '2024-10-04 18:44:35.526014'
item: 'false'
msg: non-zero return code
rc: 1
start: '2024-10-04 18:44:35.523267'
stderr: ''
stderr_lines: <omitted>
stdout: ''
stdout_lines: <omitted>
TASK [Reporting failure in processing] ****************************
ok: [localhost] =>
msg: We processed commands until `false`
Or Ansible core version 2.18.
You can skip all subsequent element of a failed (or skipped) elements, using a when
condition.
Given:
- block:
- name: Run commands until the first failure
ansible.builtin.command: "{{ item }}"
loop:
- 'true'
- 'false'
- 'ls'
when:
- not _command.skipped | default(false)
- not _command.failed | default(false)
register: _command
- debug:
msg: All commands were processed
rescue:
- debug:
msg: >-
We processed commands until
`{{ (_command.results | default([]) | select('failed'))[0].item }}`
You will get:
TASK [Run commands until the first failure] ***********************
changed: [localhost] => (item=true)
failed: [localhost] (item=false) => changed=true
ansible_loop_var: item
cmd:
- 'false'
delta: '0:00:00.002522'
end: '2024-10-04 19:06:47.998590'
item: 'false'
msg: non-zero return code
rc: 1
start: '2024-10-04 19:06:47.996068'
stderr: ''
stderr_lines: <omitted>
stdout: ''
stdout_lines: <omitted>
skipping: [localhost] => (item=true)
TASK [debug] ******************************************************
ok: [localhost] =>
msg: We processed commands until `false`
Note that: having to implement those conditions could end up being complex if you have to deal with existing condition(s) on your task to skip item(s) due to other reason(s).
Which is exactly the rational for the implementation of the loop_control:break_when
feature.