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

Improve jinja2 rule error handling #3039

Merged
merged 1 commit into from
Feb 15, 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
155 changes: 85 additions & 70 deletions src/ansiblelint/rules/jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import black
import jinja2
from ansible.errors import AnsibleError, AnsibleFilterError, AnsibleParserError
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.parsing.yaml.objects import AnsibleUnicode
from jinja2.exceptions import TemplateSyntaxError

Expand All @@ -29,6 +29,19 @@

Token = namedtuple("Token", "lineno token_type value")

ignored_re = re.compile(
"|".join(
[
r"^Object of type method is not JSON serializable",
r"^Unexpected templating type error occurred on",
r"^obj must be a list of dicts or a nested dict$",
r"^the template file (.*) could not be found for the lookup$",
r"could not locate file in lookup",
r"unable to locate collection",
]
)
)


class JinjaRule(AnsibleLintRule):
"""Rule that looks inside jinja2 templates."""
Expand All @@ -55,80 +68,82 @@ def _msg(self, tag: str, value: str, reformatted: str) -> str:
def matchtask( # noqa: C901
self, task: dict[str, Any], file: Lintable | None = None
) -> bool | str | MatchError:
for key, v, _ in nested_items_path(task):
if isinstance(v, str):
try:
template(
basedir=file.dir if file else ".",
value=v,
variables=deannotate(task.get("vars", {})),
fail_on_error=True, # we later decide which ones to ignore or not
)
# ValueError RepresenterError
except AnsibleError as exc:
bypass = False
orig_exc = exc.orig_exc if getattr(exc, "orig_exc", None) else exc
match = self._ansible_error_re.match(
getattr(orig_exc, "message", str(orig_exc))
)
if (
isinstance(exc, AnsibleFilterError)
or "unable to locate collection" in orig_exc.message
):
bypass = True
elif re.match(
r"^the template file (.*) could not be found for the lookup$",
orig_exc.message,
) or re.match(r"could not locate file in lookup", orig_exc.message):
bypass = True
elif isinstance(orig_exc, AnsibleParserError):
# "An unhandled exception occurred while running the lookup plugin '...'. Error was a <class 'ansible.errors.AnsibleParserError'>, original message: Invalid filename: 'None'. Invalid filename: 'None'"

# An unhandled exception occurred while running the lookup plugin 'template'. Error was a <class 'ansible.errors.AnsibleError'>, original message: the template file ... could not be found for the lookup. the template file ... could not be found for the lookup

# ansible@devel (2.14) new behavior:
# AnsibleError(TemplateSyntaxError): template error while templating string: Could not load "ipwrap": 'Invalid plugin FQCN (ansible.netcommon.ipwrap): unable to locate collection ansible.netcommon'. String: Foo {{ buildset_registry.host | ipwrap }}. Could not load "ipwrap": 'Invalid plugin FQCN (ansible.netcommon.ipwrap): unable to locate collection ansible.netcommon'
bypass = True
elif (
isinstance(orig_exc, (AnsibleError, TemplateSyntaxError))
and match
):
error = match.group("error")
detail = match.group("detail")
# string = match.group("string")
if error.startswith("template error while templating string"):
bypass = False
elif detail.startswith("unable to locate collection"):
_logger.debug("Ignored AnsibleError: %s", exc)
try:
for key, v, _ in nested_items_path(task):
if isinstance(v, str):
try:
template(
basedir=file.dir if file else ".",
value=v,
variables=deannotate(task.get("vars", {})),
fail_on_error=True, # we later decide which ones to ignore or not
)
# ValueError RepresenterError
except AnsibleError as exc:
bypass = False
orig_exc = (
exc.orig_exc if getattr(exc, "orig_exc", None) else exc
)
orig_exc_message = getattr(orig_exc, "message", str(orig_exc))
match = self._ansible_error_re.match(
getattr(orig_exc, "message", str(orig_exc))
)
if ignored_re.match(orig_exc_message):
bypass = True
elif isinstance(orig_exc, AnsibleParserError):
# "An unhandled exception occurred while running the lookup plugin '...'. Error was a <class 'ansible.errors.AnsibleParserError'>, original message: Invalid filename: 'None'. Invalid filename: 'None'"

# An unhandled exception occurred while running the lookup plugin 'template'. Error was a <class 'ansible.errors.AnsibleError'>, original message: the template file ... could not be found for the lookup. the template file ... could not be found for the lookup

# ansible@devel (2.14) new behavior:
# AnsibleError(TemplateSyntaxError): template error while templating string: Could not load "ipwrap": 'Invalid plugin FQCN (ansible.netcommon.ipwrap): unable to locate collection ansible.netcommon'. String: Foo {{ buildset_registry.host | ipwrap }}. Could not load "ipwrap": 'Invalid plugin FQCN (ansible.netcommon.ipwrap): unable to locate collection ansible.netcommon'
bypass = True
elif (
isinstance(orig_exc, (AnsibleError, TemplateSyntaxError))
and match
):
error = match.group("error")
detail = match.group("detail")
# string = match.group("string")
if error.startswith(
"template error while templating string"
):
bypass = False
elif detail.startswith("unable to locate collection"):
_logger.debug("Ignored AnsibleError: %s", exc)
bypass = True
else:
bypass = False
elif re.match(r"^lookup plugin (.*) not found$", exc.message):
# lookup plugin 'template' not found
bypass = True
else:
bypass = False
elif re.match(r"^lookup plugin (.*) not found$", exc.message):
# lookup plugin 'template' not found
bypass = True

# AnsibleFilterError: 'obj must be a list of dicts or a nested dict'
# AnsibleError: template error while templating string: expected token ':', got '}'. String: {{ {{ '1' }} }}
# AnsibleError: template error while templating string: unable to locate collection ansible.netcommon. String: Foo {{ buildset_registry.host | ipwrap }}
if not bypass:

# AnsibleFilterError: 'obj must be a list of dicts or a nested dict'
# AnsibleError: template error while templating string: expected token ':', got '}'. String: {{ {{ '1' }} }}
# AnsibleError: template error while templating string: unable to locate collection ansible.netcommon. String: Foo {{ buildset_registry.host | ipwrap }}
if not bypass:
return self.create_matcherror(
message=str(exc),
linenumber=task[LINE_NUMBER_KEY],
filename=file,
tag=f"{self.id}[invalid]",
)
reformatted, details, tag = self.check_whitespace(
v, key=key, lintable=file
)
if reformatted != v:
return self.create_matcherror(
message=str(exc),
message=self._msg(
tag=tag, value=v, reformatted=reformatted
),
linenumber=task[LINE_NUMBER_KEY],
details=details,
filename=file,
tag=f"{self.id}[invalid]",
tag=f"{self.id}[{tag}]",
)

reformatted, details, tag = self.check_whitespace(
v, key=key, lintable=file
)
if reformatted != v:
return self.create_matcherror(
message=self._msg(tag=tag, value=v, reformatted=reformatted),
linenumber=task[LINE_NUMBER_KEY],
details=details,
filename=file,
tag=f"{self.id}[{tag}]",
)
except Exception as exc:
_logger.info("Exception in JinjaRule.matchtask: %s", exc)
raise
return False

def matchyaml(self, file: Lintable) -> list[MatchError]:
Expand Down
2 changes: 0 additions & 2 deletions src/ansiblelint/rules/role_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ def matchyaml(self, file: Lintable) -> list[MatchError]:

if file.kind == "playbook":
for play in file.data:
# breakpoint()
if "roles" in play:
line = play["__line__"]
for role in play["roles"]:
Expand All @@ -100,7 +99,6 @@ def matchyaml(self, file: Lintable) -> list[MatchError]:
elif isinstance(role, str):
role_name = role
if "/" in role_name:
# breakpoint()
result.append(
self.create_matcherror(
f"Avoid using paths when importing roles. ({role_name})",
Expand Down