Support Jinja imports with relative paths. Issue #13889

Jinja imports will be interpreted as originating from the top 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.

Prior practices required the following construct:

.. code-block:: yaml

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

This patch supports a more "natural" construct:

.. code-block:: yaml

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

Comparatively when importing from a parent directory - prior practice:

.. code-block:: yaml

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

Construct supported by this patch:

.. code-block:: yaml

  {% from '../foo' import bar %}
This commit is contained in:
Thayne Harbaugh 2018-05-04 17:14:01 -06:00
parent 6147f08df7
commit 485e151507
7 changed files with 70 additions and 13 deletions

View file

@ -98,20 +98,52 @@ class SaltCacheLoader(BaseLoader):
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.
self.check_cache(template)
Jinja imports will be interpreted as originating from the top
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
)
print('Relative path "{0}" cannot be resolved without an environment'.format(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 file roots', template
)
raise TemplateNotFound(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('/', '.'),
}
@ -119,7 +151,7 @@ class SaltCacheLoader(BaseLoader):
# 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)
@ -205,7 +237,7 @@ def skip_filter(data):
'''
Suppress data output
.. code-balock:: yaml
.. code-block:: yaml
{% my_string = "foo" %}

View file

@ -332,7 +332,7 @@ class SaltTestingParser(optparse.OptionParser):
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))
print_header(u'', inline=True, width=self.options.output_columns)
self.pre_execution_cleanup()

View file

@ -0,0 +1 @@
FAILURE

View file

@ -0,0 +1 @@
{% import '../../rescape' as xfail -%}

View file

@ -0,0 +1,2 @@
{% from './rmacro' import rmacro with context -%}
{{ rmacro('Hey') ~ rmacro(a|default('a'), b|default('b')) }}

View file

@ -0,0 +1,4 @@
{% from '../macro' import mymacro with context %}
{% macro rmacro(greeting, greetee='world') -%}
{{ mymacro(greeting, greetee) }}
{%- endmacro %}

View file

@ -170,6 +170,23 @@ class TestSaltCacheLoader(TestCase):
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