Search code examples
ansible

How to break out of an Ansible loop?


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?


Solution

  • Breaking out of a loop, generally speaking

    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.


    As of Ansible version 11.0

    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 a when 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 the when 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 a break_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.


    Prior to Ansible version 11.0

    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) 
    

    Breaking out of a loop when an item fails

    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.


    As of Ansible version 11.0

    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`
    

    Prior to Ansible version 11.0

    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.