diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 0000000000..ae61a0f2c7 --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,25 @@ +# Molecule is not really a collection but this file is use for testing molecule +# ability to detect and install a local collection. +namespace: _test +name: molecule +version: 0.0.1 +readme: README.rst +authors: + - Ansible +build_ignore: + - "**/*.egg-info" + - "**/.mypy_cache" + - .cache + - .coverage + - .eggs + - .github + - .gitignore + - .pytest_cache + - .vscode + - asset + - build + - coverage.xml + - dist + - docs/docstree/html + - pip-wheel-metadata + - tox.ini diff --git a/src/molecule/dependency/ansible_galaxy/__init__.py b/src/molecule/dependency/ansible_galaxy/__init__.py index 2a1a7f5a29..2031f4249c 100644 --- a/src/molecule/dependency/ansible_galaxy/__init__.py +++ b/src/molecule/dependency/ansible_galaxy/__init__.py @@ -72,6 +72,7 @@ def __init__(self, config): self.invocations = [Roles(config), Collections(config)] def execute(self): + super().execute() for invoker in self.invocations: invoker.execute() @@ -85,7 +86,7 @@ def _has_requirements_file(self): def default_env(self): e = {} for invoker in self.invocations: - e = util.merge(e, invoker.default_env) + e = util.merge_dicts(e, invoker.default_env) return e @property diff --git a/src/molecule/dependency/base.py b/src/molecule/dependency/base.py index 420f312dc7..67b8376822 100644 --- a/src/molecule/dependency/base.py +++ b/src/molecule/dependency/base.py @@ -21,7 +21,10 @@ import abc import os +import re import time +from pathlib import Path +from subprocess import CalledProcessError from molecule import constants, util from molecule.logger import get_logger @@ -80,13 +83,36 @@ def execute_with_retries(self): LOG.error(str(exception), self._sh_command) util.sysexit(getattr(exception, "exit_code", constants.RC_UNKNOWN_ERROR)) - @abc.abstractmethod - def execute(self): # pragma: no cover + def execute(self): """ Execute ``cmd`` and returns None. :return: None """ + collection_marker = os.path.join(util.find_vcs_root(), "galaxy.yml") + if os.path.isfile(collection_marker): + try: + LOG.info("Collection detected at %s", os.getcwd()) + + dist = Path(self._config.scenario.ephemeral_directory) / "dist" + dist.mkdir(parents=True, exist_ok=True) + result = util.run_command( + f"ansible-galaxy collection build -v -f --output-path {dist}", + env=self.default_env, + check=True, + echo=True, + ) + archive = re.search(r"([^\s]+\.tar\.gz)$", result.stdout).groups()[0] + # no need to specify destination path because Molecule already defines + # custom isolated ANSIBLE_COLLECTIONS_PATH for each scenario + result = util.run_command( + f"ansible-galaxy collection install -f {archive}", + env=self.default_env, + check=True, + echo=True, + ) + except CalledProcessError as e: + util.sysexit_with_message(e, e.returncode) @abc.abstractproperty def default_options(self): # pragma: no cover diff --git a/src/molecule/dependency/shell.py b/src/molecule/dependency/shell.py index 0e23a4f368..858120e266 100644 --- a/src/molecule/dependency/shell.py +++ b/src/molecule/dependency/shell.py @@ -88,6 +88,7 @@ def bake(self) -> None: self._sh_command = BakedCommand(cmd=self.command, env=self.env) def execute(self): + super().execute() if not self.enabled: msg = "Skipping, dependency is disabled." LOG.warning(msg) diff --git a/src/molecule/util.py b/src/molecule/util.py index 0bd91b5166..292fd9aa94 100644 --- a/src/molecule/util.py +++ b/src/molecule/util.py @@ -28,7 +28,7 @@ import sys from dataclasses import dataclass from functools import lru_cache # noqa -from subprocess import CompletedProcess +from subprocess import CalledProcessError, CompletedProcess from typing import Any, Dict, List, MutableMapping, NoReturn, Optional, Union import jinja2 @@ -109,7 +109,7 @@ def sysexit_with_message( def run_command( - cmd, env=None, debug=False, echo=False, quiet=False + cmd, env=None, debug=False, echo=False, quiet=False, check=False ) -> CompletedProcess: """ Execute the given command and returns None. @@ -118,7 +118,6 @@ def run_command( - a string or list of strings (similar to subprocess.run) - a BakedCommand object ( :param debug: An optional bool to toggle debug output. - :return: ``sh`` object """ args = [] stdout = None @@ -139,9 +138,17 @@ def run_command( if debug: print_environment_vars(env) - return run( + result = run( args, env=env, stdout=stdout, stderr=stderr, echo=echo or debug, quiet=quiet ) + if result.returncode != 0 and check: + raise CalledProcessError( + returncode=result.returncode, + cmd=result.args, + output=result.stdout, + stderr=result.stderr, + ) + return result def os_walk(directory, pattern, excludes=[]):