diff --git a/changelogs/fragments/75435-creating-Undefined.yml b/changelogs/fragments/75435-creating-Undefined.yml new file mode 100644 index 00000000000..3e2f8bc578a --- /dev/null +++ b/changelogs/fragments/75435-creating-Undefined.yml @@ -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. diff --git a/docs/docsite/rst/user_guide/playbooks_filters.rst b/docs/docsite/rst/user_guide/playbooks_filters.rst index ed3baee6db1..2c1d3976fd7 100644 --- a/docs/docsite/rst/user_guide/playbooks_filters.rst +++ b/docs/docsite/rst/user_guide/playbooks_filters.rst @@ -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) ======================================================= diff --git a/lib/ansible/plugins/filter/core.py b/lib/ansible/plugins/filter/core.py index 164290c3baa..ea62ff728d1 100644 --- a/lib/ansible/plugins/filter/core.py +++ b/lib/ansible/plugins/filter/core.py @@ -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 diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py index f5c0c9cc9c6..4bb4296363b 100644 --- a/lib/ansible/template/__init__.py +++ b/lib/ansible/template/__init__.py @@ -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 diff --git a/test/integration/targets/filter_core/tasks/main.yml b/test/integration/targets/filter_core/tasks/main.yml index c637fc1754c..55b2881c309 100644 --- a/test/integration/targets/filter_core/tasks/main.yml +++ b/test/integration/targets/filter_core/tasks/main.yml @@ -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: diff --git a/test/integration/targets/template/runme.sh b/test/integration/targets/template/runme.sh index a4f0bbe522f..78f8d7b5fb8 100755 --- a/test/integration/targets/template/runme.sh +++ b/test/integration/targets/template/runme.sh @@ -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 "$@" diff --git a/test/integration/targets/undefined/tasks/main.yml b/test/integration/targets/undefined/tasks/main.yml index de6681a0879..bbd82845852 100644 --- a/test/integration/targets/undefined/tasks/main.yml +++ b/test/integration/targets/undefined/tasks/main.yml @@ -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 diff --git a/test/units/plugins/filter/test_core.py b/test/units/plugins/filter/test_core.py index 8a626d9a75c..df4e4725def 100644 --- a/test/units/plugins/filter/test_core.py +++ b/test/units/plugins/filter/test_core.py @@ -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