Skip to content

Commit

Permalink
Add rules for verifying the existence of imported and included files
Browse files Browse the repository at this point in the history
PR #691 by @jlusiardi

This new rule checks if the files referenced in import and include
actually exist and emits an error if not. This can be switched off
with a noqa comment (e.g. on files that depend on a variable).

Signed-off-by: Joachim Lusiardi <joachim@lusiardi.de>
Signed-off-by: Sviatoslav Sydorenko <webknjaz@redhat.com>

Co-authored-by: Joachim Lusiardi <joachim@lusiardi.de>
Co-authored-by: Sviatoslav Sydorenko <webknjaz@redhat.com>
  • Loading branch information
jlusiardi and webknjaz authored Mar 18, 2020
1 parent b978b1f commit 41d597d
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/docsite/rst/rules/default_rules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ ID
`E502 <https://github.com/ansible/ansible-lint/blob/master/lib/ansiblelint/rules/TaskHasNameRule.py>`_ historic All tasks should be named All tasks should have a distinct name for readability and for ``--start-at-task`` to work
`E503 <https://github.com/ansible/ansible-lint/blob/master/lib/ansiblelint/rules/UseHandlerRatherThanWhenChangedRule.py>`_ historic Tasks that run when changed should likely be handlers If a task has a ``when: result.changed`` setting, it is effectively acting as a handler
`E504 <https://github.com/ansible/ansible-lint/blob/master/lib/ansiblelint/rules/TaskNoLocalAction.py>`_ v4.0.0 Do not use 'local_action', use 'delegate_to: localhost' Do not use ``local_action``, use ``delegate_to: localhost``
`E505 <https://github.com/ansible/ansible-lint/blob/master/lib/ansiblelint/rules/IncludeMissingFileRule.py>`_ v4.3.0 referenced files must exist All files referenced by by include / import tasks must exist. The check excludes files with jinja2 templates in the filename.

**E6xx - idiom**
`E601 <https://github.com/ansible/ansible-lint/blob/master/lib/ansiblelint/rules/ComparisonToLiteralBoolRule.py>`_ v4.0.0 Don't compare to literal True/False Use ``when: var`` rather than ``when: var == True`` (or conversely ``when: not var``)
Expand Down
6 changes: 5 additions & 1 deletion lib/ansiblelint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,11 @@ def run(self):
continue
if playbook[1] == 'role':
continue
files.append({'path': ansiblelint.utils.normpath(playbook[0]), 'type': playbook[1]})
files.append({'path': ansiblelint.utils.normpath(playbook[0]),
'type': playbook[1],
# add an absolute path here, so rules are able to validate if
# referenced files exist
'absolute_directory': os.path.dirname(playbook[0])})
visited = set()
while (visited != self.playbooks):
for arg in self.playbooks - visited:
Expand Down
66 changes: 66 additions & 0 deletions lib/ansiblelint/rules/IncludeMissingFileRule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright (c) 2020, Joachim Lusiardi
# Copyright (c) 2020, Ansible Project

from __future__ import print_function

import os.path

import ansible.parsing.yaml.objects

from ansiblelint import AnsibleLintRule


class IncludeMissingFileRule(AnsibleLintRule):
id = '505'
shortdesc = 'referenced files must exist'
description = (
'All files referenced by by include / import tasks '
'must exist. The check excludes files with jinja2 '
'templates in the filename.'
)
severity = 'MEDIUM'
tags = ['task', 'bug']
version_added = 'v4.3.0'

def matchplay(self, file, data):
absolute_directory = file.get('absolute_directory', None)
results = []
for task in data.get('tasks', []):

# check if the id of the current rule is not in list of skipped rules for this play
if self.id in task['skipped_rules']:
continue

# collect information which file was referenced for include / import
referenced_file = None
for key, val in task.items():
if not (key.startswith('include_') or
key.startswith('import_') or
key == 'include'):
continue
if isinstance(val, ansible.parsing.yaml.objects.AnsibleMapping):
referenced_file = val.get('file', None)
else:
referenced_file = val
# take the file and skip the remaining keys
if referenced_file:
break

if referenced_file is None:
continue

# make sure we have a absolute path here and check if it is a file
referenced_file = os.path.join(absolute_directory, referenced_file)

# skip if this is a jinja2 templated reference
if '{{' in referenced_file:
continue

# existing files do not produce any error
if os.path.isfile(referenced_file):
continue

results.append(({'referenced_file': referenced_file},
'referenced missing file in %s:%i'
% (task['__file__'], task['__line__'])))
return results
84 changes: 84 additions & 0 deletions test/TestIncludeMissingFileRule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from collections import namedtuple
import os

import pytest

from ansiblelint import Runner, RulesCollection


PlayFile = namedtuple('PlayFile', ['name', 'content'])


PLAY_INCLUDING_PLAIN = PlayFile('playbook.yml', u'''
- hosts: all
tasks:
- include: some_file.yml
''')

PLAY_INCLUDING_JINJA2 = PlayFile('playbook.yml', u'''
- hosts: all
tasks:
- include: "{{ some_path }}/some_file.yml"
''')

PLAY_INCLUDING_NOQA = PlayFile('playbook.yml', u'''
- hosts: all
tasks:
- include: some_file.yml # noqa 505
''')

PLAY_INCLUDED = PlayFile('some_file.yml', u'''
- debug:
msg: 'was found & included'
''')


@pytest.fixture
def play_file_path(tmp_path):
p = tmp_path / 'playbook.yml'
return str(p)


@pytest.fixture
def rules():
rulesdir = os.path.join('lib', 'ansiblelint', 'rules')
return RulesCollection([rulesdir])


@pytest.fixture
def runner(play_file_path, rules):
return Runner(rules, play_file_path, [], [], [])


@pytest.fixture
def play_files(tmp_path, request):
if request.param is None:
return
for play_file in request.param:
p = tmp_path / play_file.name
p.write_text(play_file.content)


@pytest.mark.parametrize(
'play_files', [pytest.param([PLAY_INCLUDING_PLAIN], id='referenced file missing')],
indirect=['play_files']
)
def test_include_file_missing(runner, play_files):
results = str(runner.run())
assert 'referenced missing file in' in results
assert 'playbook.yml' in results
assert 'some_file.yml' in results


@pytest.mark.parametrize(
'play_files',
[
pytest.param([PLAY_INCLUDING_PLAIN, PLAY_INCLUDED], id='File Exists'),
pytest.param([PLAY_INCLUDING_JINJA2], id='JINJA2 in reference'),
pytest.param([PLAY_INCLUDING_NOQA], id='NOQA was used')
],
indirect=['play_files']
)
def test_cases_that_do_not_report(runner, play_files):
results = str(runner.run())
assert 'referenced missing file in' not in results

0 comments on commit 41d597d

Please sign in to comment.