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

Add ability to mock modules from collections #1316

Merged
merged 1 commit into from Feb 8, 2021
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
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