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

Support Jinja imports with relative paths. Issue #13889 #47490

Merged
merged 16 commits into from
Jun 8, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions doc/topics/releases/fluorine.rst
Original file line number Diff line number Diff line change
Expand Up @@ -594,3 +594,33 @@ This release add an additional search using the ``groupattribute`` field as
well. The original ``accountattributename`` search is done first then the
``groupattribute`` allowing for backward compatibility with previous Salt
releases.

Jinja Include Relative Paths
============================

When a jinja include template name begins with ``./`` or
``../`` then the import will be relative to the importing file.

Prior practices required the following construct:

.. code-block:: jinja

{% from tpldir ~ '/foo' import bar %}

A more "natural" construct is now supported:

.. code-block:: jinja

{% from './foo' import bar %}

Comparatively when importing from a parent directory - prior practice:

.. code-block:: jinja

{% from tpldir ~ '/../foo' import bar %}

New style for including from a parent directory:

.. code-block:: jinja

{% from '../foo' import bar %}
55 changes: 43 additions & 12 deletions salt/utils/jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,28 +98,59 @@ def check_cache(self, template):
self.cached.append(template)

def get_source(self, environment, template):
# checks for relative '..' paths
if '..' in template:
log.warning(
'Discarded template path \'%s\', relative paths are '
'prohibited', template
)
raise TemplateNotFound(template)
'''
Salt-specific loader to find imported jinja files.

Jinja imports will be interpreted as originating from the top
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import behavior needs to be in the documentation. Where is the best location for it?

of each of the directories in the searchpath when the template
name does not begin with './' or '../'. When a template name
begins with './' or '../' then the import will be relative to
the importing file.

'''
# FIXME: somewhere do seprataor replacement: '\\' => '/'
_template = template
if template.split('/', 1)[0] in ('..', '.'):
is_relative = True
else:
is_relative = False
# checks for relative '..' paths that step-out of file_roots
if is_relative:
# Starts with a relative path indicator

if not environment or 'tpldir' not in environment.globals:
log.warning(
'Relative path "%s" cannot be resolved without an environment',
template
)
raise TemplateNotFound
base_path = environment.globals['tpldir']
_template = os.path.normpath('/'.join((base_path, _template)))
if _template.split('/', 1)[0] == '..':
log.warning(
'Discarded template path "%s": attempts to'
' ascend outside of salt://', template
)
raise TemplateNotFound(template)

self.check_cache(template)
self.check_cache(_template)

if environment and template:
tpldir = os.path.dirname(template).replace('\\', '/')
tpldir = os.path.dirname(_template).replace('\\', '/')
tplfile = _template
if is_relative:
tpldir = environment.globals.get('tpldir', tpldir)
tplfile = template
tpldata = {
'tplfile': template,
'tplfile': tplfile,
'tpldir': '.' if tpldir == '' else tpldir,
'tpldot': tpldir.replace('/', '.'),
}
environment.globals.update(tpldata)

# pylint: disable=cell-var-from-loop
for spath in self.searchpath:
filepath = os.path.join(spath, template)
filepath = os.path.join(spath, _template)
try:
with salt.utils.files.fopen(filepath, 'rb') as ifile:
contents = ifile.read().decode(self.encoding)
Expand Down Expand Up @@ -205,7 +236,7 @@ def skip_filter(data):
'''
Suppress data output

.. code-balock:: yaml
.. code-block:: yaml

{% my_string = "foo" %}

Expand Down
2 changes: 1 addition & 1 deletion tests/support/parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ def parse_args(self, args=None, values=None):
os.path.basename(fpath).startswith('test_'):
self.options.name.append(fpath)
continue
self.exit(status=1, msg='\'{}\' is not a valid test module'.format(fpath))
self.exit(status=1, msg='\'{}\' is not a valid test module\n'.format(fpath))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me understand this change? I don't know what adding a newline here fixes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the newline then the error message is not on a line by itself and the command prompt is left on the same line as the error message.


print_header(u'', inline=True, width=self.options.output_columns)
self.pre_execution_cleanup()
Expand Down
1 change: 1 addition & 0 deletions tests/unit/templates/files/rescape
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FAILURE
1 change: 1 addition & 0 deletions tests/unit/templates/files/test/relative/rescape
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% import '../../rescape' as xfail -%}
2 changes: 2 additions & 0 deletions tests/unit/templates/files/test/relative/rhello
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% from './rmacro' import rmacro with context -%}
{{ rmacro('Hey') ~ rmacro(a|default('a'), b|default('b')) }}
4 changes: 4 additions & 0 deletions tests/unit/templates/files/test/relative/rmacro
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% from '../macro' import mymacro with context %}
{% macro rmacro(greeting, greetee='world') -%}
{{ mymacro(greeting, greetee) }}
{%- endmacro %}
17 changes: 17 additions & 0 deletions tests/unit/templates/test_jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,23 @@ def test_import(self):
self.assertEqual(fc.requests[0]['path'], 'salt://hello_import')
self.assertEqual(fc.requests[1]['path'], 'salt://macro')

def test_relative_import(self):
'''
You can import using relative paths
issue-13889
'''
fc, jinja = self.get_test_saltenv()
tmpl = jinja.get_template('relative/rhello')
result = tmpl.render()
self.assertEqual(result, 'Hey world !a b !')
assert len(fc.requests) == 3
self.assertEqual(fc.requests[0]['path'], 'salt://relative/rhello')
self.assertEqual(fc.requests[1]['path'], 'salt://relative/rmacro')
self.assertEqual(fc.requests[2]['path'], 'salt://macro')
# This must fail when rendered: attempts to import from outside file root
template = jinja.get_template('relative/rescape')
self.assertRaises(exceptions.TemplateNotFound, template.render)

def test_include(self):
'''
You can also include a template that imports and uses macros
Expand Down