Add an `undef` global Jinja function (#75435)

* add tests for fail filter

also tests that fail does not block inspectability

* add fail filter

fallback message is a bit clunky,
since you can't invoke a filter without specifying an input.
That is, "{{ fail }}" doesn't work,
so you have to do "{{ None | fail }}"

* document 'fail' filter

* add changelog fragment

* fail filter uses default message on Undefined or emptystring

makes it slightly easier to use the default message:
```diff
- "{{ None | fail }}"
+ "{{ '' | fail }}"
```

and the user sees a slightly more relevant message
if the message itself is undefined:

```diff
- The error was: {{ failmsg | fail }}: 'failmsg' is undefined
+ The error was: {{ failmsg | fail }}: Mandatory variable has not been overridden
```

* rebuild as the builtin `Undefined`

* harmonise `hint` parameter for make_undefined with jinja

* use code block for documentation item

[ref](https://github.com/ansible/ansible/pull/75435#discussion_r707661035)

* rename to `undef` to expose less Python into the Jinja

[ref](https://github.com/ansible/ansible/pull/75435#pullrequestreview-757799031)

* explicitly instantiate undefined value now that it's possible

see I knew we would break something with reflection

* preserve test coverage of undefined variable

Co-authored-by: Matt Davis <nitzmahone@users.noreply.github.com>
pull/75743/head
Daniel Goldman 3 years ago committed by GitHub
parent 47b644570f
commit 989eeb243f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
minor_changes:
- adds the ``undef`` keyword to the templating environment. This allows for directly creating Undefined values in templates. It is most useful for providing a hint for variables which must be overridden.

@ -68,6 +68,13 @@ If you configure Ansible to ignore undefined variables, you may want to define s
The variable value will be used as is, but the template evaluation will raise an error if it is undefined.
A convenient way of requiring a variable to be overridden is to give it an undefined value using the ``undef`` keyword. This can be useful in a role's defaults.
.. code-block:: yaml+jinja
galaxy_url: "https://galaxy.ansible.com"
galaxy_api_key: {{ undef(hint="You must specify your Galaxy API key") }}
Defining different values for true/false/null (ternary)
=======================================================

@ -32,7 +32,7 @@ from ansible.module_utils.common._collections_compat import Mapping
from ansible.module_utils.common.yaml import yaml_load, yaml_load_all
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.parsing.yaml.dumper import AnsibleDumper
from ansible.template import recursive_check_defined
from ansible.template import AnsibleUndefined, recursive_check_defined
from ansible.utils.display import Display
from ansible.utils.encrypt import passlib_or_crypt
from ansible.utils.hashing import md5s, checksum_s

@ -693,6 +693,7 @@ class Templar:
self.environment.globals['query'] = self.environment.globals['q'] = self._query_lookup
self.environment.globals['now'] = self._now_datetime
self.environment.globals['finalize'] = self._finalize
self.environment.globals['undef'] = self._make_undefined
# the current rendering context under which the templar class is working
self.cur_context = None
@ -1068,6 +1069,13 @@ class Templar:
return ran
def _make_undefined(self, hint=None):
from jinja2.runtime import Undefined
if hint is None or isinstance(hint, Undefined) or hint == '':
hint = "Mandatory variable has not been overridden"
return AnsibleUndefined(hint)
def do_template(self, data, preserve_trailing_newlines=True, escape_backslashes=True, fail_on_undefined=None, overrides=None, disable_lookups=False):
if self.jinja2_native and not isinstance(data, string_types):
return data

@ -494,6 +494,39 @@
- mandatory_2 is failed
- "mandatory_2.msg == 'You did not give me a variable. I am a sad wolf.'"
- name: Verify undef throws if resolved
set_fact:
foo: '{{ fail_foo }}'
vars:
fail_foo: '{{ undef("Expected failure") }}'
ignore_errors: yes
register: fail_1
- name: Setup fail_foo for overriding in test
block:
- name: Verify undef not executed if overridden
set_fact:
foo: '{{ fail_foo }}'
vars:
fail_foo: 'overridden value'
register: fail_2
vars:
fail_foo: '{{ undef(hint="Expected failure") }}'
- name: Verify undef is inspectable
debug:
var: fail_foo
vars:
fail_foo: '{{ undef("Expected failure") }}'
register: fail_3
- name: Verify undef
assert:
that:
- fail_1 is failed
- not (fail_2 is failed)
- not (fail_3 is failed)
- name: Verify comment
assert:
that:

@ -4,8 +4,8 @@ set -eux
ANSIBLE_ROLES_PATH=../ ansible-playbook template.yml -i ../../inventory -v "$@"
# Test for #35571
ansible testhost -i testhost, -m debug -a 'msg={{ hostvars["localhost"] }}' -e "vars1={{ undef }}" -e "vars2={{ vars1 }}"
# Test for https://github.com/ansible/ansible/pull/35571
ansible testhost -i testhost, -m debug -a 'msg={{ hostvars["localhost"] }}' -e "vars1={{ undef() }}" -e "vars2={{ vars1 }}"
# Test for https://github.com/ansible/ansible/issues/27262
ansible-playbook ansible_managed.yml -c ansible_managed.cfg -i ../../inventory -v "$@"

@ -11,7 +11,8 @@
- assert:
that:
- '"%r"|format(undef) == "AnsibleUndefined"'
- '"%r"|format(an_undefined_var) == "AnsibleUndefined"'
- '"%r"|format(undef()) == "AnsibleUndefined"'
# The existence of AnsibleUndefined in a templating result
# prevents safe_eval from turning the value into a python object
- names is string

@ -3,6 +3,8 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
from jinja2.runtime import Undefined
from jinja2.exceptions import UndefinedError
__metaclass__ = type
import pytest

Loading…
Cancel
Save