Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't mutate templar.environment, only overlay on local myenv #81005

Merged
merged 10 commits into from Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions 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
41 changes: 26 additions & 15 deletions lib/ansible/plugins/action/template.py
Expand Up @@ -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
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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:
Expand Down
17 changes: 11 additions & 6 deletions lib/ansible/plugins/lookup/template.py
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
65 changes: 38 additions & 27 deletions lib/ansible/template/__init__.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
'''
Expand Down Expand Up @@ -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 "\\\\".
Expand Down
4 changes: 4 additions & 0 deletions 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 >>
28 changes: 0 additions & 28 deletions test/integration/targets/template/in_template_overrides.yml

This file was deleted.

2 changes: 1 addition & 1 deletion test/integration/targets/template/runme.sh
Expand Up @@ -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 "$@"

Expand Down
38 changes: 38 additions & 0 deletions 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"