diff --git a/changelogs/fragments/56017-allow-lazy-eval-on-jinja2-expr.yml b/changelogs/fragments/56017-allow-lazy-eval-on-jinja2-expr.yml new file mode 100644 index 00000000000..e2fae50bde6 --- /dev/null +++ b/changelogs/fragments/56017-allow-lazy-eval-on-jinja2-expr.yml @@ -0,0 +1,2 @@ +breaking_changes: + - Allow for lazy evaluation of Jinja2 expressions (https://github.com/ansible/ansible/issues/56017) diff --git a/docs/docsite/rst/porting_guides/porting_guide_core_2.14.rst b/docs/docsite/rst/porting_guides/porting_guide_core_2.14.rst index af833303211..6e0cf9bca0d 100644 --- a/docs/docsite/rst/porting_guides/porting_guide_core_2.14.rst +++ b/docs/docsite/rst/porting_guides/porting_guide_core_2.14.rst @@ -19,7 +19,19 @@ This document is part of a collection on porting. The complete list of porting g Playbook ======== -No notable changes +* Variables are now evaluated lazily; only when they are actually used. For example, in ansible-core 2.14 an expression ``{{ defined_variable or undefined_variable }}`` does not fail on ``undefined_variable`` if the first part of ``or`` is evaluated to ``True`` as it is not needed to evaluate the second part. One particular case of a change in behavior to note is the task below which uses the ``undefined`` test. Prior to version 2.14 this would result in a fatal error trying to access the undefined value in the dictionary. In 2.14 the assertion passes as the dictionary is evaluated as undefined through one of its undefined values: + + .. code-block:: yaml + + - assert: + that: + - some_defined_dict_with_undefined_values is undefined + vars: + dict_value: 1 + some_defined_dict_with_undefined_values: + key1: value1 + key2: '{{ dict_value }}' + key3: '{{ undefined_dict_value }}' Command Line diff --git a/lib/ansible/template/vars.py b/lib/ansible/template/vars.py index b91638f25d9..be2af9fc3b8 100644 --- a/lib/ansible/template/vars.py +++ b/lib/ansible/template/vars.py @@ -97,7 +97,15 @@ class AnsibleJ2Vars(Mapping): try: value = self._templar.template(variable) except AnsibleUndefinedVariable as e: - raise AnsibleUndefinedVariable("%s: %s" % (to_native(variable), e.message)) + # Instead of failing here prematurely, return an Undefined + # object which fails only after its first usage allowing us to + # do lazy evaluation and passing it into filters/tests that + # operate on such objects. + return self._templar.environment.undefined( + hint=f"{variable}: {e.message}", + name=varname, + exc=AnsibleUndefinedVariable, + ) except Exception as e: msg = getattr(e, 'message', None) or to_native(e) raise AnsibleError("An unhandled exception occurred while templating '%s'. " diff --git a/test/integration/targets/template/lazy_eval.yml b/test/integration/targets/template/lazy_eval.yml new file mode 100644 index 00000000000..1acd36c7256 --- /dev/null +++ b/test/integration/targets/template/lazy_eval.yml @@ -0,0 +1,24 @@ +- hosts: testhost + gather_facts: false + vars: + deep_undefined: "{{ nested_undefined_variable }}" + tasks: + - name: These do not throw an error, deep_undefined is just evaluated to undefined, since 2.13 + assert: + that: + - lazy_eval or deep_undefined + - deep_undefined is undefined + - deep_undefined|default('defaulted') == 'defaulted' + vars: + lazy_eval: true + + - name: EXPECTED FAILURE actually using deep_undefined fails + debug: + msg: "{{ deep_undefined }}" + ignore_errors: true + register: res + + - assert: + that: + - res.failed + - res.msg is contains("'nested_undefined_variable' is undefined") diff --git a/test/integration/targets/template/runme.sh b/test/integration/targets/template/runme.sh index 78f8d7b5fb8..14cfaf992bd 100755 --- a/test/integration/targets/template/runme.sh +++ b/test/integration/targets/template/runme.sh @@ -40,3 +40,5 @@ ansible-playbook unsafe.yml -v "$@" # ensure Jinja2 overrides from a template are used ansible-playbook in_template_overrides.yml -v "$@" + +ansible-playbook lazy_eval.yml -i ../../inventory -v "$@" diff --git a/test/units/playbook/test_conditional.py b/test/units/playbook/test_conditional.py index 17284ca2258..8729ba9e4b3 100644 --- a/test/units/playbook/test_conditional.py +++ b/test/units/playbook/test_conditional.py @@ -121,19 +121,6 @@ class TestConditional(unittest.TestCase): self._eval_con, when, variables) - def test_dict_undefined_values_is_defined(self): - variables = {'dict_value': 1, - 'some_defined_dict_with_undefined_values': {'key1': 'value1', - 'key2': '{{ dict_value }}', - 'key3': '{{ undefined_dict_value }}' - }} - - when = [u"some_defined_dict_with_undefined_values is defined"] - self.assertRaisesRegex(errors.AnsibleError, - "The conditional check 'some_defined_dict_with_undefined_values is defined' failed.", - self._eval_con, - when, variables) - def test_is_defined(self): variables = {'some_defined_thing': True} when = [u"some_defined_thing is defined"]