Skip to content

Commit

Permalink
Add ability to mock modules from collections
Browse files Browse the repository at this point in the history
Fixes: #1254
Fixes: #538
  • Loading branch information
ssbarnea committed Feb 8, 2021
1 parent 43788ce commit cbdd1cd
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 26 deletions.
1 change: 1 addition & 0 deletions .ansible-lint
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions examples/playbooks/mocked_dependency.yml
Expand Up @@ -4,3 +4,5 @@
tasks:
- name: some task
zuul_return: {}
- name: mocked module from collection
fake_namespace.fake_collection.fake_module: {}
67 changes: 50 additions & 17 deletions 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,
Expand Down Expand Up @@ -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()

Expand All @@ -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:
Expand Down
58 changes: 57 additions & 1 deletion 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.
Expand Down Expand Up @@ -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(':')
33 changes: 26 additions & 7 deletions src/ansiblelint/constants.py
Expand Up @@ -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[
Expand Down
1 change: 0 additions & 1 deletion tox.ini
Expand Up @@ -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
Expand Down

0 comments on commit cbdd1cd

Please sign in to comment.