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

Mock jinja filters to prevent templating errors #3355

Merged
merged 1 commit into from Apr 26, 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
3 changes: 2 additions & 1 deletion src/ansiblelint/runner.py
Expand Up @@ -196,7 +196,8 @@ def _emit_matches(self, files: list[Lintable]) -> Generator[MatchError, None, No
while visited != self.lintables:
for lintable in self.lintables - visited:
try:
for child in ansiblelint.utils.find_children(lintable):
children = ansiblelint.utils.find_children(lintable)
for child in children:
if self.is_excluded(child):
continue
self.lintables.add(child)
Expand Down
85 changes: 65 additions & 20 deletions src/ansiblelint/utils.py
Expand Up @@ -113,37 +113,82 @@ def ansible_templar(basedir: str, templatevars: Any) -> Templar:
return templar


def mock_filter(left: Any, *args: Any, **kwargs: Any) -> Any:
"""Mock a filter that can take any combination of args and kwargs.

This will return x when x | filter(y,z) is called
e.g. {{ foo | ansible.utils.ipaddr('address') }}

:param left: The left hand side of the filter
:param args: The args passed to the filter
:param kwargs: The kwargs passed to the filter
:return: The left hand side of the filter
"""
# pylint: disable=unused-argument
return left


def ansible_template(
basedir: str,
varname: Any,
templatevars: Any,
**kwargs: Any,
) -> Any:
"""Render a templated string by mocking missing filters."""
"""Render a templated string by mocking missing filters.

In the case of a missing lookup, ansible core does an early exit
when disable_lookup=True but this happens after the jinja2 syntax already passed
return the original string as if it had been templated.

In the case of a missing filter, extract the missing filter plugin name
from the ansible error, 'Could not load "filter"'. Then mock the filter
and template the string again. The range allows for up to 10 unknown filters
in succession

:param basedir: The directory containing the lintable file
:param varname: The string to be templated
:param templatevars: The variables to be used in the template
:param kwargs: Additional arguments to be passed to the templating engine
:return: The templated string or None
:raises: AnsibleError if the filter plugin cannot be extracted or the
string could not be templated in 10 attempts
"""
# pylint: disable=too-many-locals
filter_error = "template error while templating string:"
lookup_error = "was found, however lookups were disabled from templating"
re_filter_fqcn = re.compile(r"\w+\.\w+\.\w+")
re_filter_in_err = re.compile(r"Could not load \"(\w+)\"")
re_valid_filter = re.compile(r"^\w+(\.\w+\.\w+)?$")
templar = ansible_templar(basedir=basedir, templatevars=templatevars)
# pylint: disable=unused-variable
for i in range(3):

kwargs["disable_lookups"] = True
for _i in range(10):
try:
kwargs["disable_lookups"] = True
return templar.template(varname, **kwargs)
templated = templar.template(varname, **kwargs)
return templated
except AnsibleError as exc:
if (
"was found, however lookups were disabled from templating"
in exc.message
):
# ansible core does an early exit when disable_lookup=True but
# this happens after the jinja2 syntax already passed.
break
if (
exc.message.startswith("template error while templating string:")
and "'" in exc.message
):
missing_filter = exc.message.split("'")[1]
if missing_filter == "end of print statement":
if lookup_error in exc.message:
return varname
if exc.message.startswith(filter_error):
while True:
match = re_filter_in_err.search(exc.message)
if match:
missing_filter = match.group(1)
break
match = re_filter_fqcn.search(exc.message)
if match:
missing_filter = match.group(0)
break
missing_filter = exc.message.split("'")[1]
break

if not re_valid_filter.match(missing_filter):
err = f"Could not parse missing filter name from error message: {exc.message}"
_logger.warning(err)
raise
# Mock the filter to avoid and error from Ansible templating

# pylint: disable=protected-access
templar.environment.filters._delegatee[missing_filter] = lambda x: x
templar.environment.filters._delegatee[missing_filter] = mock_filter
# Record the mocked filter so we can warn the user
if missing_filter not in options.mock_filters:
_logger.debug("Mocking missing filter %s", missing_filter)
Expand Down