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. 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) 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.module_utils.common.yaml import yaml_load, yaml_load_all
from ansible.parsing.ajson import AnsibleJSONEncoder from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.parsing.yaml.dumper import AnsibleDumper 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.display import Display
from ansible.utils.encrypt import passlib_or_crypt from ansible.utils.encrypt import passlib_or_crypt
from ansible.utils.hashing import md5s, checksum_s 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['query'] = self.environment.globals['q'] = self._query_lookup
self.environment.globals['now'] = self._now_datetime self.environment.globals['now'] = self._now_datetime
self.environment.globals['finalize'] = self._finalize self.environment.globals['finalize'] = self._finalize
self.environment.globals['undef'] = self._make_undefined
# the current rendering context under which the templar class is working # the current rendering context under which the templar class is working
self.cur_context = None self.cur_context = None
@ -1068,6 +1069,13 @@ class Templar:
return ran 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): 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): if self.jinja2_native and not isinstance(data, string_types):
return data return data

@ -494,6 +494,39 @@
- mandatory_2 is failed - mandatory_2 is failed
- "mandatory_2.msg == 'You did not give me a variable. I am a sad wolf.'" - "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 - name: Verify comment
assert: assert:
that: that:

@ -4,8 +4,8 @@ set -eux
ANSIBLE_ROLES_PATH=../ ansible-playbook template.yml -i ../../inventory -v "$@" ANSIBLE_ROLES_PATH=../ ansible-playbook template.yml -i ../../inventory -v "$@"
# Test for #35571 # 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 }}" 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 # Test for https://github.com/ansible/ansible/issues/27262
ansible-playbook ansible_managed.yml -c ansible_managed.cfg -i ../../inventory -v "$@" ansible-playbook ansible_managed.yml -c ansible_managed.cfg -i ../../inventory -v "$@"

@ -11,7 +11,8 @@
- assert: - assert:
that: that:
- '"%r"|format(undef) == "AnsibleUndefined"' - '"%r"|format(an_undefined_var) == "AnsibleUndefined"'
- '"%r"|format(undef()) == "AnsibleUndefined"'
# The existence of AnsibleUndefined in a templating result # The existence of AnsibleUndefined in a templating result
# prevents safe_eval from turning the value into a python object # prevents safe_eval from turning the value into a python object
- names is string - 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) # 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 __future__ import absolute_import, division, print_function
from jinja2.runtime import Undefined
from jinja2.exceptions import UndefinedError
__metaclass__ = type __metaclass__ = type
import pytest import pytest

Loading…
Cancel
Save