diff --git a/.ansible-lint b/.ansible-lint index caf18ee17d..3e6906d179 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -8,6 +8,7 @@ exclude_paths: # Mock modules or roles in order to pass ansible-playbook --syntax-check mock_modules: - zuul_return + - fake_namespace.fake_collection.fake_module mock_roles: - mocked_role diff --git a/examples/playbooks/mocked_dependency.yml b/examples/playbooks/mocked_dependency.yml index 31844d7aee..ae489fdbc4 100644 --- a/examples/playbooks/mocked_dependency.yml +++ b/examples/playbooks/mocked_dependency.yml @@ -4,3 +4,5 @@ tasks: - name: some task zuul_return: {} + - name: mocked module from collection + fake_namespace.fake_collection.fake_module: {} diff --git a/src/ansiblelint/_prerun.py b/src/ansiblelint/_prerun.py index c7b25496f0..fd9e96be21 100644 --- a/src/ansiblelint/_prerun.py +++ b/src/ansiblelint/_prerun.py @@ -1,11 +1,11 @@ import os import subprocess import sys -from typing import List +from typing import List, Optional from packaging import version -from ansiblelint.config import options +from ansiblelint.config import ansible_collections_path, collection_list, options from ansiblelint.constants import ( ANSIBLE_MIN_VERSION, ANSIBLE_MISSING_RC, @@ -84,13 +84,6 @@ def prepare_environment() -> None: if run.returncode != 0: sys.exit(run.returncode) - if 'ANSIBLE_COLLECTIONS_PATHS' in os.environ: - os.environ[ - 'ANSIBLE_COLLECTIONS_PATHS' - ] = f".cache/collections:{os.environ['ANSIBLE_COLLECTIONS_PATHS']}" - else: - os.environ['ANSIBLE_COLLECTIONS_PATHS'] = ".cache/collections" - _prepare_library_paths() _prepare_roles_path() @@ -105,16 +98,56 @@ def _prepare_library_paths() -> None: library_paths.append("plugins/modules") if options.mock_modules: - library_paths.append(".cache/modules") - os.makedirs(".cache/modules", exist_ok=True) for module_name in options.mock_modules: - with open(f".cache/modules/{module_name}.py", "w") as f: - f.write(ANSIBLE_MOCKED_MODULE) + _make_module_stub(module_name) + if os.path.exists(".cache/collections"): + collection_list.append(".cache/collections") + if os.path.exists(".cache/modules"): + library_paths.append(".cache/modules") + + _update_env('ANSIBLE_LIBRARY', library_paths) + _update_env(ansible_collections_path(), collection_list) + + +def _make_module_stub(module_name: str) -> None: + if "." not in module_name: + os.makedirs(".cache/modules", exist_ok=True) + _write_module_stub( + filename=f".cache/modules/{module_name}.py", name=module_name + ) + else: + namespace, collection, module_file = module_name.split(".") + path = f".cache/collections/ansible_collections/{ namespace }/{ collection }/plugins/modules" + os.makedirs(path, exist_ok=True) + _write_module_stub( + filename=f"{path}/{module_file}.py", + name=module_file, + namespace=namespace, + collection=collection, + ) + - library_path_str = ":".join(library_paths) - if library_path_str != os.environ.get('ANSIBLE_LIBRARY', ""): - os.environ['ANSIBLE_LIBRARY'] = library_path_str - print("Added ANSIBLE_LIBRARY=%s" % library_path_str, file=sys.stderr) +def _write_module_stub( + filename: str, + name: str, + namespace: Optional[str] = None, + collection: Optional[str] = None, +) -> None: + """Write module stub to disk.""" + body = ANSIBLE_MOCKED_MODULE.format( + name=name, collection=collection, namespace=namespace + ) + with open(filename, "w") as f: + f.write(body) + + +def _update_env(varname: str, value: List[str]) -> None: + """Update environment variable if needed.""" + if value: + value_str = ":".join(value) + if value_str != os.environ.get(varname, ""): + os.environ[varname] = value_str + print("Added %s=%s" % (varname, value_str), file=sys.stderr) def _prepare_roles_path() -> None: diff --git a/src/ansiblelint/config.py b/src/ansiblelint/config.py index 3429832025..eb5c99b060 100644 --- a/src/ansiblelint/config.py +++ b/src/ansiblelint/config.py @@ -1,6 +1,14 @@ """Store configuration options as a singleton.""" +import os +import subprocess +import sys from argparse import Namespace -from typing import Dict +from functools import lru_cache +from typing import Dict, List + +from packaging.version import Version + +from ansiblelint.constants import ANSIBLE_MISSING_RC DEFAULT_KINDS = [ # Do not sort this list, order matters. @@ -45,3 +53,51 @@ # Used to store detected tag deprecations used_old_tags: Dict[str, str] = {} + +# Used to store collection list paths (with mock paths if needed) +collection_list: List[str] = [] + + +@lru_cache() +def ansible_collections_path() -> str: + """Return collection path variable for current version of Ansible.""" + # respect Ansible behavior, which is to load old name if present + for env_var in ["ANSIBLE_COLLECTIONS_PATHS", "ANSIBLE_COLLECTIONS_PATH"]: + if "ANSIBLE_COLLECTIONS_PATHS" in os.environ: + return env_var + + # https://github.com/ansible/ansible/pull/70007 + if ansible_version() >= ansible_version("2.10.0.dev0"): + return "ANSIBLE_COLLECTIONS_PATH" + return "ANSIBLE_COLLECTIONS_PATHS" + + +@lru_cache() +def ansible_version(version: str = "") -> Version: + """Return current Version object for Ansible. + + If version is not mentioned, it returns current version as detected. + When version argument is mentioned, it return converts the version string + to Version object in order to make it usable in comparisons. + """ + if not version: + proc = subprocess.run( + ["ansible", "--version"], + universal_newlines=True, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if proc.returncode == 0: + version = proc.stdout.splitlines()[0].split()[1] + else: + print( + "Unable to find a working copy of ansible executable.", + proc, + ) + sys.exit(ANSIBLE_MISSING_RC) + return Version(version) + + +if ansible_collections_path() in os.environ: + collection_list = os.environ[ansible_collections_path()].split(':') diff --git a/src/ansiblelint/constants.py b/src/ansiblelint/constants.py index 2be4783d1b..4f236fa045 100644 --- a/src/ansiblelint/constants.py +++ b/src/ansiblelint/constants.py @@ -20,18 +20,37 @@ ANSIBLE_MIN_VERSION = "2.9" ANSIBLE_MOCKED_MODULE = """\ -# This is a mocked Ansible module +# This is a mocked Ansible module generated by ansible-lint from ansible.module_utils.basic import AnsibleModule +DOCUMENTATION = ''' +module: {name} + +short_description: Mocked +version_added: "1.0.0" +description: Mocked + +author: + - ansible-lint (@nobody) +''' +EXAMPLES = '''mocked''' +RETURN = '''mocked''' def main(): - return AnsibleModule( - argument_spec=dict( - data=dict(default=None), - path=dict(default=None, type=str), - file=dict(default=None, type=str), - ) + result = dict( + changed=False, + original_message='', + message='') + + module = AnsibleModule( + argument_spec=dict(), + supports_check_mode=True, ) + module.exit_json(**result) + + +if __name__ == "__main__": + main() """ FileType = Literal[ diff --git a/tox.ini b/tox.ini index be68159580..178c83dabc 100644 --- a/tox.ini +++ b/tox.ini @@ -53,7 +53,6 @@ passenv = SSL_CERT_FILE # https proxies # recreate = True setenv = - ANSIBLE_COLLECTIONS_PATHS = {envtmpdir} COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}} PIP_DISABLE_PIP_VERSION_CHECK = 1 PRE_COMMIT_COLOR = always