diff --git a/.config/dictionary.txt b/.config/dictionary.txt index 895fb4cc415..1b633b2bca3 100644 --- a/.config/dictionary.txt +++ b/.config/dictionary.txt @@ -270,6 +270,8 @@ robertdebock rolepath roundtrip ruamel +rulebook +rulebooks ruledirs rulesdir rulesdirs diff --git a/examples/.config/molecule/config.yml b/examples/.config/molecule/config.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/other/guess-1.yml b/examples/other/guess-1.yml new file mode 100644 index 00000000000..b19971883ea --- /dev/null +++ b/examples/other/guess-1.yml @@ -0,0 +1,3 @@ +--- +# minimal yaml to determine it as a playbook +- hosts: localhost diff --git a/examples/playbooks/rulebook.yml b/examples/playbooks/rulebook.yml new file mode 100644 index 00000000000..3eaf3089281 --- /dev/null +++ b/examples/playbooks/rulebook.yml @@ -0,0 +1,21 @@ +--- +# That file is not a valid playbook but it is a valid rulebook that was +# mistakenly put under a playbook directory. +- name: Demo rules with kafka as source + hosts: localhost + sources: + - name: kafka + kafka: + topic: eda + host: localhost + port: 9092 + group_id: testing + rules: + - name: + condition: event.i is defined + action: + debug: + - name: + condition: event.stop == true + action: + shutdown: diff --git a/examples/roles/foo.yml b/examples/roles/foo.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/rulebooks/rulebook.yml b/examples/rulebooks/rulebook.yml new file mode 100644 index 00000000000..1f5d444144b --- /dev/null +++ b/examples/rulebooks/rulebook.yml @@ -0,0 +1,19 @@ +--- +- name: Demo rules with kafka as source + hosts: localhost + sources: + - name: kafka + kafka: + topic: eda + host: localhost + port: 9092 + group_id: testing + rules: + - name: + condition: event.i is defined + action: + debug: + - name: + condition: event.stop == true + action: + shutdown: diff --git a/src/ansiblelint/config.py b/src/ansiblelint/config.py index 7b5d1866de4..c86275d0ca9 100644 --- a/src/ansiblelint/config.py +++ b/src/ansiblelint/config.py @@ -56,6 +56,7 @@ {"galaxy": "**/galaxy.yml"}, # Galaxy collection meta {"reno": "**/releasenotes/*/*.{yaml,yml}"}, # reno release notes {"tasks": "**/tasks/**/*.{yaml,yml}"}, + {"rulebook": "**/rulebooks/*.{yml,yaml"}, {"playbook": "**/playbooks/*.{yml,yaml}"}, {"playbook": "**/*playbook*.{yml,yaml}"}, {"role": "**/roles/*/"}, diff --git a/src/ansiblelint/constants.py b/src/ansiblelint/constants.py index 0ec7c7a2698..0fec8fdd6ac 100644 --- a/src/ansiblelint/constants.py +++ b/src/ansiblelint/constants.py @@ -53,6 +53,7 @@ def main(): FileType = Literal[ "playbook", + "rulebook", "meta", # role meta "meta-runtime", "tasks", # includes pre_tasks, post_tasks diff --git a/src/ansiblelint/file_utils.py b/src/ansiblelint/file_utils.py index e5c7a593025..c456cc9729e 100644 --- a/src/ansiblelint/file_utils.py +++ b/src/ansiblelint/file_utils.py @@ -241,6 +241,24 @@ def __init__( self.base_kind = base_kind or kind_from_path(self.path, base=True) self.abspath = self.path.expanduser().absolute() + if self.kind in ("yaml", None): + self._guess_kind() + + def _guess_kind(self) -> None: + if self.kind == "yaml": + if isinstance(self.data, list) and "hosts" in self.data[0]: + if "rules" not in self.data[0]: + self.kind = "playbook" + else: + self.kind = "rulebook" + # we we failed to guess the more specific kind, we warn user + if self.kind == "yaml": + _logger.warning( + "Passed '%s' positional argument was identified as generic '%s' file kind.", + self.name, + self.kind, + ) + def __getitem__(self, key: Any) -> Any: """Provide compatibility subscriptable support.""" if key == "path": @@ -497,4 +515,8 @@ def expand_dirs_in_lintables(lintables: set[Lintable]) -> None: if item.path.is_dir(): for filename in all_files: if filename.startswith(str(item.path)): - lintables.add(Lintable(filename)) + try: + lintables.add(Lintable(filename)) + except Exception as exc: + breakpoint() + pass diff --git a/src/ansiblelint/rules/name.py b/src/ansiblelint/rules/name.py index f6e82e08bb5..ba34f8a3b80 100644 --- a/src/ansiblelint/rules/name.py +++ b/src/ansiblelint/rules/name.py @@ -117,14 +117,12 @@ def _check_name( return results else: effective_name = name[len(prefix) :] - # breakpoint() if ( effective_name[0].isalpha() and effective_name[0].islower() and not effective_name[0].isupper() ): - # breakpoint() results.append( self.create_matcherror( message="All names should start with an uppercase letter.", diff --git a/src/ansiblelint/schemas/main.py b/src/ansiblelint/schemas/main.py index 81aab82598c..97f9eb9462b 100644 --- a/src/ansiblelint/schemas/main.py +++ b/src/ansiblelint/schemas/main.py @@ -90,7 +90,6 @@ def refresh_schemas(min_age_seconds: int = 3600 * 24) -> int: _logger.debug("Checking for updated schemas...") changed = 0 - # breakpoint() for kind, data in JSON_SCHEMAS.items(): url = data["url"] if "#" in url: diff --git a/src/ansiblelint/utils.py b/src/ansiblelint/utils.py index 4054cab00bd..51c8bfbde23 100644 --- a/src/ansiblelint/utils.py +++ b/src/ansiblelint/utils.py @@ -846,14 +846,6 @@ def get_lintables( if args: for arg in args: lintable = Lintable(arg) - if lintable.kind in ("yaml", None): - _logger.warning( - "Overriding detected file kind '%s' with 'playbook' " - "for given positional argument: %s", - lintable.kind, - arg, - ) - lintable = Lintable(arg, kind="playbook") lintables.append(lintable) else: diff --git a/test/local-content/test-collection.yml b/test/local-content/test-collection.yml index 0de3b195e37..47b097d51f0 100644 --- a/test/local-content/test-collection.yml +++ b/test/local-content/test-collection.yml @@ -5,6 +5,6 @@ - name: Use module from local collection testns.test_collection.test_module_2: - name: Use filter from local collection - assert: + ansible.builtin.assert: that: - 1 | testns.test_collection.test_filter(2) == '1:2' diff --git a/test/schemas/test/molecule/cluster/base.yml b/test/schemas/test/molecule/cluster/base.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/schemas/test/molecule/cluster/converge.yml b/test/schemas/test/molecule/cluster/converge.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/schemas/test/molecule/cluster/foobar.yml b/test/schemas/test/molecule/cluster/foobar.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_file_utils.py b/test/test_file_utils.py index 8a75e271de5..ed0ee6f5a6b 100644 --- a/test/test_file_utils.py +++ b/test/test_file_utils.py @@ -147,20 +147,23 @@ def test_discover_lintables_umlaut(monkeypatch: MonkeyPatch) -> None: ("tasks/run_test_playbook.yml", "tasks"), ("foo/playbook.yml", "playbook"), ("playbooks/foo.yml", "playbook"), - ("playbooks/roles/foo.yml", "yaml"), + ("examples/roles/foo.yml", "yaml"), # the only yml file that is not a playbook inside molecule/ folders - (".config/molecule/config.yml", "yaml"), # molecule shared config + ("examples/.config/molecule/config.yml", "yaml"), # molecule shared config ( - "roles/foo/molecule/scenario1/base.yml", + "test/schemas/test/molecule/cluster/base.yml", "yaml", ), # molecule scenario base config ( - "roles/foo/molecule/scenario1/molecule.yml", + "test/schemas/test/molecule/cluster/molecule.yml", "yaml", ), # molecule scenario config - ("roles/foo/molecule/scenario2/foobar.yml", "playbook"), # custom playbook name ( - "roles/foo/molecule/scenario3/converge.yml", + "test/schemas/test/molecule/cluster/foobar.yml", + "playbook", + ), # custom playbook name + ( + "test/schemas/test/molecule/cluster/converge.yml", "playbook", ), # common playbook name ( @@ -212,9 +215,6 @@ def mockreturn(options: Namespace) -> dict[str, Any]: monkeypatch.setattr(file_utils, "discover_lintables", mockreturn) result = file_utils.discover_lintables(options) - # get_lintable could return additional files and we only care to see - # that the given file is among the returned list. - assert lintable_detected.name in result assert lintable_detected.kind == result[lintable_expected.name] @@ -308,10 +308,8 @@ def test_lintable_content_setter_with_bad_types(updated_content: Any) -> None: def test_lintable_with_new_file(tmp_path: Path) -> None: """Validate ``Lintable.updated`` for a new file.""" - lintable = Lintable(tmp_path / "new.yaml") - with pytest.raises(FileNotFoundError): - _ = lintable.content + lintable = Lintable(tmp_path / "new.yaml") lintable.content = BASIC_PLAYBOOK assert lintable.content == BASIC_PLAYBOOK @@ -460,3 +458,38 @@ def test_bug_2513( results = Runner(filename, rules=default_rules_collection).run() assert len(results) == 1 assert results[0].rule.id == "name" + + +@pytest.mark.parametrize( + ("path", "kind"), + ( + pytest.param( + "examples/playbooks/rulebook.yml", "playbook", id="0" + ), # playbooks folder should determite kind + pytest.param( + "examples/rulebooks/rulebook.yml", "rulebook", id="1" + ), # content should determine it as a rulebook + pytest.param( + "examples/yamllint/valid.yml", "yaml", id="2" + ), # empty yaml is valid yaml, not assuming anything else + pytest.param( + "examples/other/guess-1.yml", "playbook", id="3" + ), # content should determine is as a playplay + pytest.param( + "examples/playbooks/tasks/passing_task.yml", "tasks", id="4" + ), # content should determine is tasks + pytest.param("examples/collection/galaxy.yml", "galaxy", id="5"), + pytest.param("examples/meta/runtime.yml", "meta-runtime", id="6"), + pytest.param("examples/meta/changelogs/changelog.yaml", "changelog", id="7"), + pytest.param("examples/inventory/inventory.yml", "inventory", id="8"), + pytest.param("examples/inventory/production.yml", "inventory", id="9"), + pytest.param("examples/playbooks/vars/empty_vars.yml", "vars", id="10"), + pytest.param( + "examples/playbooks/vars/subfolder/settings.yaml", "vars", id="11" + ), + ), +) +def test_lintable_guess_kind(path: str, kind: str) -> None: + """Test that Lintable can guess kind of file.""" + lintable = Lintable(path) + assert lintable.kind == kind diff --git a/test/test_formatter.py b/test/test_formatter.py index 964dbe940ab..29153f852b7 100644 --- a/test/test_formatter.py +++ b/test/test_formatter.py @@ -38,7 +38,7 @@ def test_format_coloured_string() -> None: message="message", linenumber=1, details=DETAILS, - filename=Lintable("filename.yml"), + filename=Lintable("filename.yml", content=""), rule=rule, ) formatter.format(match) @@ -50,7 +50,7 @@ def test_unicode_format_string() -> None: message="\U0001f427", linenumber=1, details=DETAILS, - filename=Lintable("filename.yml"), + filename=Lintable("filename.yml", content=""), rule=rule, ) formatter.format(match) @@ -62,7 +62,7 @@ def test_dict_format_line() -> None: message="xyz", linenumber=1, details={"hello": "world"}, # type: ignore - filename=Lintable("filename.yml"), + filename=Lintable("filename.yml", content=""), rule=rule, ) formatter.format(match) diff --git a/test/test_formatter_json.py b/test/test_formatter_json.py index bf276095636..9e995aa5f18 100644 --- a/test/test_formatter_json.py +++ b/test/test_formatter_json.py @@ -32,7 +32,7 @@ def setup_class(self) -> None: message="message", linenumber=1, details="hello", - filename=Lintable("filename.yml"), + filename=Lintable("filename.yml", content=""), rule=self.rule, ) ) @@ -41,7 +41,7 @@ def setup_class(self) -> None: message="message", linenumber=2, details="hello", - filename=Lintable("filename.yml"), + filename=Lintable("filename.yml", content=""), rule=self.rule, ) ) @@ -103,7 +103,7 @@ def test_validate_codeclimate_schema_with_positions(self) -> None: linenumber=1, column=42, details="hello", - filename=Lintable("filename.yml"), + filename=Lintable("filename.yml", content=""), rule=self.rule, ) ] diff --git a/test/test_formatter_sarif.py b/test/test_formatter_sarif.py index 9509d6ce2b0..86cc45154d7 100644 --- a/test/test_formatter_sarif.py +++ b/test/test_formatter_sarif.py @@ -36,7 +36,7 @@ def setup_class(self) -> None: linenumber=1, column=10, details="hello", - filename=Lintable("filename.yml"), + filename=Lintable("filename.yml", content=""), rule=self.rule, tag="yaml[test]", ) @@ -46,7 +46,7 @@ def setup_class(self) -> None: message="message", linenumber=2, details="hello", - filename=Lintable("filename.yml"), + filename=Lintable("filename.yml", content=""), rule=self.rule, tag="yaml[test]", ) diff --git a/test/test_matcherrror.py b/test/test_matcherrror.py index d3e893e28c1..041b7ce0af3 100644 --- a/test/test_matcherrror.py +++ b/test/test_matcherrror.py @@ -77,8 +77,8 @@ def test_matcherror_invalid() -> None: (MatchError("z"), MatchError("a")), # filenames takes priority in sorting ( - MatchError("a", filename=Lintable("b")), - MatchError("a", filename=Lintable("a")), + MatchError("a", filename=Lintable("b", content="")), + MatchError("a", filename=Lintable("a", content="")), ), # rule id partial-become > rule id no-changed-when ( diff --git a/test/test_yaml_utils.py b/test/test_yaml_utils.py index 2a276c7ae56..c5b4d20331f 100644 --- a/test/test_yaml_utils.py +++ b/test/test_yaml_utils.py @@ -23,8 +23,7 @@ @pytest.fixture(name="empty_lintable") def fixture_empty_lintable() -> Lintable: """Return a Lintable with no contents.""" - lintable = Lintable("__empty_file__.yaml") - lintable._content = "" # pylint: disable=protected-access + lintable = Lintable("__empty_file__.yaml", content="") return lintable