loop_control "early exit" feature (#62151)

* add a loop_control break_when directive to break out of a loop after any item

* remove loop var as normal exit would

* example usage:

- name: generate a random password up to 10 times, until it matches the policy
  set_fact:
    password: "{{ lookup('password', '/dev/null', chars=character_set, length=length) }}"
  loop: "{{ range(0, 10) }}"
  loop_control:
    break_when:
      - password is match(password_policy)

Co-authored-by: s-hertel <19572925+s-hertel@users.noreply.github.com>
pull/83908/head
Brian Coca 3 months ago committed by GitHub
parent e3ccdaaa2e
commit 4fa512406b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,2 @@
minor_changes:
- loop_control - add a break_when option to to break out of a task loop early based on Jinja2 expressions (https://github.com/ansible/ansible/issues/83442).

@ -402,6 +402,19 @@ class TaskExecutor:
self._final_q.send_callback('v2_runner_item_on_ok', tr) self._final_q.send_callback('v2_runner_item_on_ok', tr)
results.append(res) results.append(res)
# break loop if break_when conditions are met
if self._task.loop_control and self._task.loop_control.break_when:
cond = Conditional(loader=self._loader)
cond.when = self._task.loop_control.get_validated_value(
'break_when', self._task.loop_control.fattributes.get('break_when'), self._task.loop_control.break_when, templar
)
if cond.evaluate_conditional(templar, task_vars):
# delete loop vars before exiting loop
del task_vars[loop_var]
break
# done with loop var, remove for next iteration
del task_vars[loop_var] del task_vars[loop_var]
# clear 'connection related' plugin variables for next iteration # clear 'connection related' plugin variables for next iteration

@ -29,6 +29,7 @@ class LoopControl(FieldAttributeBase):
pause = NonInheritableFieldAttribute(isa='float', default=0, always_post_validate=True) pause = NonInheritableFieldAttribute(isa='float', default=0, always_post_validate=True)
extended = NonInheritableFieldAttribute(isa='bool', always_post_validate=True) extended = NonInheritableFieldAttribute(isa='bool', always_post_validate=True)
extended_allitems = NonInheritableFieldAttribute(isa='bool', default=True, always_post_validate=True) extended_allitems = NonInheritableFieldAttribute(isa='bool', default=True, always_post_validate=True)
break_when = NonInheritableFieldAttribute(isa='list', default=list)
def __init__(self): def __init__(self):
super(LoopControl, self).__init__() super(LoopControl, self).__init__()
@ -37,3 +38,10 @@ class LoopControl(FieldAttributeBase):
def load(data, variable_manager=None, loader=None): def load(data, variable_manager=None, loader=None):
t = LoopControl() t = LoopControl()
return t.load_data(data, variable_manager=variable_manager, loader=loader) return t.load_data(data, variable_manager=variable_manager, loader=loader)
def _post_validate_break_when(self, attr, value, templar):
'''
break_when is evaluated after the execution of the loop is complete,
and should not be templated during the regular post_validate step.
'''
return value

@ -0,0 +1,17 @@
- hosts: localhost
gather_facts: false
tasks:
- debug: var=item
changed_when: false
loop:
- 1
- 2
- 3
- 4
loop_control:
break_when: item >= 2
register: untiltest
- assert:
that:
- untiltest['results']|length == 2

@ -10,3 +10,5 @@ bar_label'
[ "$(ansible-playbook label.yml "$@" |grep 'item='|sed -e 's/^.*(item=looped_var \(.*\)).*$/\1/')" == "${MATCH}" ] [ "$(ansible-playbook label.yml "$@" |grep 'item='|sed -e 's/^.*(item=looped_var \(.*\)).*$/\1/')" == "${MATCH}" ]
ansible-playbook extended.yml "$@" ansible-playbook extended.yml "$@"
ansible-playbook break_when.yml "$@"

@ -164,9 +164,11 @@ class TestTaskExecutor(unittest.TestCase):
def _copy(exclude_parent=False, exclude_tasks=False): def _copy(exclude_parent=False, exclude_tasks=False):
new_item = MagicMock() new_item = MagicMock()
new_item.loop_control = MagicMock(break_when=[])
return new_item return new_item
mock_task = MagicMock() mock_task = MagicMock()
mock_task.loop_control = MagicMock(break_when=[])
mock_task.copy.side_effect = _copy mock_task.copy.side_effect = _copy
mock_play_context = MagicMock() mock_play_context = MagicMock()

Loading…
Cancel
Save