diff --git a/changelogs/fragments/81005-use-overlay-overrides.yml b/changelogs/fragments/81005-use-overlay-overrides.yml new file mode 100644 index 00000000000000..149abf2f9d3509 --- /dev/null +++ b/changelogs/fragments/81005-use-overlay-overrides.yml @@ -0,0 +1,2 @@ +bugfixes: +- templating - In the template action and lookup, use local jinja2 environment overlay overrides instead of mutating the templars environment diff --git a/lib/ansible/plugins/action/template.py b/lib/ansible/plugins/action/template.py index 426c53f03f99ba..4bfd967053b9be 100644 --- a/lib/ansible/plugins/action/template.py +++ b/lib/ansible/plugins/action/template.py @@ -10,6 +10,15 @@ import stat import tempfile +from jinja2.defaults import ( + BLOCK_END_STRING, + BLOCK_START_STRING, + COMMENT_END_STRING, + COMMENT_START_STRING, + VARIABLE_END_STRING, + VARIABLE_START_STRING, +) + from ansible import constants as C from ansible.config.manager import ensure_type from ansible.errors import AnsibleError, AnsibleFileNotFound, AnsibleAction, AnsibleActionFail @@ -57,12 +66,12 @@ def run(self, tmp=None, task_vars=None): dest = self._task.args.get('dest', None) state = self._task.args.get('state', None) newline_sequence = self._task.args.get('newline_sequence', self.DEFAULT_NEWLINE_SEQUENCE) - variable_start_string = self._task.args.get('variable_start_string', None) - variable_end_string = self._task.args.get('variable_end_string', None) - block_start_string = self._task.args.get('block_start_string', None) - block_end_string = self._task.args.get('block_end_string', None) - comment_start_string = self._task.args.get('comment_start_string', None) - comment_end_string = self._task.args.get('comment_end_string', None) + variable_start_string = self._task.args.get('variable_start_string', VARIABLE_START_STRING) + variable_end_string = self._task.args.get('variable_end_string', VARIABLE_END_STRING) + block_start_string = self._task.args.get('block_start_string', BLOCK_START_STRING) + block_end_string = self._task.args.get('block_end_string', BLOCK_END_STRING) + comment_start_string = self._task.args.get('comment_start_string', COMMENT_START_STRING) + comment_end_string = self._task.args.get('comment_end_string', COMMENT_END_STRING) output_encoding = self._task.args.get('output_encoding', 'utf-8') or 'utf-8' wrong_sequences = ["\\n", "\\r", "\\r\\n"] @@ -129,16 +138,18 @@ def run(self, tmp=None, task_vars=None): templar = self._templar.copy_with_new_env(environment_class=AnsibleEnvironment, searchpath=searchpath, newline_sequence=newline_sequence, - block_start_string=block_start_string, - block_end_string=block_end_string, - variable_start_string=variable_start_string, - variable_end_string=variable_end_string, - comment_start_string=comment_start_string, - comment_end_string=comment_end_string, - trim_blocks=trim_blocks, - lstrip_blocks=lstrip_blocks, available_variables=temp_vars) - resultant = templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False) + overrides = dict( + block_start_string=block_start_string, + block_end_string=block_end_string, + variable_start_string=variable_start_string, + variable_end_string=variable_end_string, + comment_start_string=comment_start_string, + comment_end_string=comment_end_string, + trim_blocks=trim_blocks, + lstrip_blocks=lstrip_blocks + ) + resultant = templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False, overrides=overrides) except AnsibleAction: raise except Exception as e: diff --git a/lib/ansible/plugins/lookup/template.py b/lib/ansible/plugins/lookup/template.py index bedbe35cd5da71..358fa1da991795 100644 --- a/lib/ansible/plugins/lookup/template.py +++ b/lib/ansible/plugins/lookup/template.py @@ -50,10 +50,12 @@ description: The string marking the beginning of a comment statement. version_added: '2.12' type: str + default: '{#' comment_end_string: description: The string marking the end of a comment statement. version_added: '2.12' type: str + default: '#}' seealso: - ref: playbook_task_paths description: Search paths used for relative templates. @@ -148,13 +150,16 @@ def run(self, terms, variables, **kwargs): vars.update(generate_ansible_template_vars(term, lookupfile)) vars.update(lookup_template_vars) - with templar.set_temporary_context(variable_start_string=variable_start_string, - variable_end_string=variable_end_string, - comment_start_string=comment_start_string, - comment_end_string=comment_end_string, - available_variables=vars, searchpath=searchpath): + with templar.set_temporary_context(available_variables=vars, searchpath=searchpath): + overrides = dict( + variable_start_string=variable_start_string, + variable_end_string=variable_end_string, + comment_start_string=comment_start_string, + comment_end_string=comment_end_string + ) res = templar.template(template_data, preserve_trailing_newlines=True, - convert_data=convert_data_p, escape_backslashes=False) + convert_data=convert_data_p, escape_backslashes=False, + overrides=overrides) if (C.DEFAULT_JINJA2_NATIVE and not jinja2_native) or not convert_data_p: # jinja2_native is true globally but off for the lookup, we need this text diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py index f389b169390d3d..ea63269de227cd 100644 --- a/lib/ansible/template/__init__.py +++ b/lib/ansible/template/__init__.py @@ -153,6 +153,39 @@ def _escape_backslashes(data, jinja_env): return data +def _create_overlay(data, overrides, jinja_env): + if overrides is None: + overrides = {} + + try: + has_override_header = data.startswith(JINJA2_OVERRIDE) + except (TypeError, AttributeError): + has_override_header = False + + if overrides or has_override_header: + overlay = jinja_env.overlay(**overrides) + else: + overlay = jinja_env + + # Get jinja env overrides from template + if has_override_header: + eol = data.find('\n') + line = data[len(JINJA2_OVERRIDE):eol] + data = data[eol + 1:] + for pair in line.split(','): + if ':' not in pair: + raise AnsibleError("failed to parse jinja2 override '%s'." + " Did you use something different from colon as key-value separator?" % pair.strip()) + (key, val) = pair.split(':', 1) + key = key.strip() + if hasattr(overlay, key): + setattr(overlay, key, ast.literal_eval(val.strip())) + else: + display.warning(f"Could not find Jinja2 environment setting to override: '{key}'") + + return data, overlay + + def is_possibly_template(data, jinja_env): """Determines if a string looks like a template, by seeing if it contains a jinja2 start delimiter. Does not guarantee that the string @@ -695,7 +728,7 @@ def template(self, variable, convert_bare=False, preserve_trailing_newlines=True variable = self._convert_bare_variable(variable) if isinstance(variable, string_types): - if not self.is_possibly_template(variable): + if not self.is_possibly_template(variable, overrides): return variable # Check to see if the string we are trying to render is just referencing a single @@ -766,8 +799,9 @@ def is_template(self, data): templatable = is_template - def is_possibly_template(self, data): - return is_possibly_template(data, self.environment) + def is_possibly_template(self, data, overrides=None): + data, env = _create_overlay(data, overrides, self.environment) + return is_possibly_template(data, env) def _convert_bare_variable(self, variable): ''' @@ -908,34 +942,11 @@ def do_template(self, data, preserve_trailing_newlines=True, escape_backslashes= if fail_on_undefined is None: fail_on_undefined = self._fail_on_undefined_errors - has_template_overrides = data.startswith(JINJA2_OVERRIDE) - try: # NOTE Creating an overlay that lives only inside do_template means that overrides are not applied # when templating nested variables in AnsibleJ2Vars where Templar.environment is used, not the overlay. # This is historic behavior that is kept for backwards compatibility. - if overrides: - myenv = self.environment.overlay(overrides) - elif has_template_overrides: - myenv = self.environment.overlay() - else: - myenv = self.environment - - # Get jinja env overrides from template - if has_template_overrides: - eol = data.find('\n') - line = data[len(JINJA2_OVERRIDE):eol] - data = data[eol + 1:] - for pair in line.split(','): - if ':' not in pair: - raise AnsibleError("failed to parse jinja2 override '%s'." - " Did you use something different from colon as key-value separator?" % pair.strip()) - (key, val) = pair.split(':', 1) - key = key.strip() - if hasattr(myenv, key): - setattr(myenv, key, ast.literal_eval(val.strip())) - else: - display.warning(f"Could not find Jinja2 environment setting to override: '{key}'") + data, myenv = _create_overlay(data, overrides, self.environment) if escape_backslashes: # Allow users to specify backslashes in playbooks as "\\" instead of as "\\\\". diff --git a/test/integration/targets/template/arg_template_overrides.j2 b/test/integration/targets/template/arg_template_overrides.j2 new file mode 100644 index 00000000000000..17a79b91462424 --- /dev/null +++ b/test/integration/targets/template/arg_template_overrides.j2 @@ -0,0 +1,4 @@ +var_a: << var_a >> +var_b: << var_b >> +var_c: << var_c >> +var_d: << var_d >> diff --git a/test/integration/targets/template/in_template_overrides.yml b/test/integration/targets/template/in_template_overrides.yml deleted file mode 100644 index 3c2d4d9900e601..00000000000000 --- a/test/integration/targets/template/in_template_overrides.yml +++ /dev/null @@ -1,28 +0,0 @@ -- hosts: localhost - gather_facts: false - vars: - var_a: "value" - var_b: "{{ var_a }}" - var_c: "<< var_a >>" - tasks: - - set_fact: - var_d: "{{ var_a }}" - - - block: - - template: - src: in_template_overrides.j2 - dest: out.txt - - - command: cat out.txt - register: out - - - assert: - that: - - "'var_a: value' in out.stdout" - - "'var_b: value' in out.stdout" - - "'var_c: << var_a >>' in out.stdout" - - "'var_d: value' in out.stdout" - always: - - file: - path: out.txt - state: absent diff --git a/test/integration/targets/template/runme.sh b/test/integration/targets/template/runme.sh index 30163af7db920c..eaaa6aa6a5c26b 100755 --- a/test/integration/targets/template/runme.sh +++ b/test/integration/targets/template/runme.sh @@ -39,7 +39,7 @@ ansible-playbook 72262.yml -v "$@" ansible-playbook unsafe.yml -v "$@" # ensure Jinja2 overrides from a template are used -ansible-playbook in_template_overrides.yml -v "$@" +ansible-playbook template_overrides.yml -v "$@" ansible-playbook lazy_eval.yml -i ../../inventory -v "$@" diff --git a/test/integration/targets/template/template_overrides.yml b/test/integration/targets/template/template_overrides.yml new file mode 100644 index 00000000000000..50cfb8f14bdc2f --- /dev/null +++ b/test/integration/targets/template/template_overrides.yml @@ -0,0 +1,38 @@ +- hosts: localhost + gather_facts: false + vars: + output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}" + var_a: "value" + var_b: "{{ var_a }}" + var_c: "<< var_a >>" + tasks: + - set_fact: + var_d: "{{ var_a }}" + + - template: + src: in_template_overrides.j2 + dest: '{{ output_dir }}/in_template_overrides.out' + + - template: + src: arg_template_overrides.j2 + dest: '{{ output_dir }}/arg_template_overrides.out' + variable_start_string: '<<' + variable_end_string: '>>' + + - command: cat '{{ output_dir }}/in_template_overrides.out' + register: in_template_overrides_out + + - command: cat '{{ output_dir }}/arg_template_overrides.out' + register: arg_template_overrides_out + + - assert: + that: + - "'var_a: value' in in_template_overrides_out.stdout" + - "'var_b: value' in in_template_overrides_out.stdout" + - "'var_c: << var_a >>' in in_template_overrides_out.stdout" + - "'var_d: value' in in_template_overrides_out.stdout" + + - "'var_a: value' in arg_template_overrides_out.stdout" + - "'var_b: value' in arg_template_overrides_out.stdout" + - "'var_c: << var_a >>' in arg_template_overrides_out.stdout" + - "'var_d: value' in arg_template_overrides_out.stdout"