diff --git a/.ansible.cfg b/.ansible.cfg index 1ee5b13f31..177f42b36c 100644 --- a/.ansible.cfg +++ b/.ansible.cfg @@ -1 +1,3 @@ # Used for testing to avoid using one from outside repository +[defaults] +host_key_checking = False diff --git a/.config/requirements-lock.txt b/.config/requirements-lock.txt index a9f2cfe419..2f753f7f73 100644 --- a/.config/requirements-lock.txt +++ b/.config/requirements-lock.txt @@ -5,38 +5,28 @@ # pip-compile --no-annotate --output-file=.config/requirements-lock.txt --resolver=backtracking --strip-extras --unsafe-package=ruamel-yaml-clib pyproject.toml # ansible-compat==4.1.2 -ansible-core==2.14.4 -arrow==1.2.3 +ansible-core==2.15.1 attrs==23.1.0 -binaryornot==0.4.4 -certifi==2022.12.7 cffi==1.15.1 -chardet==5.1.0 -charset-normalizer==3.1.0 -click==8.1.3 +click==8.1.4 click-help-colors==0.9.1 -cookiecutter==2.1.1 -cryptography==41.0.0 +cryptography==41.0.1 enrich==1.2.7 -idna==3.4 +importlib-resources==5.0.7 jinja2==3.1.2 -jinja2-time==0.2.0 -jsonschema==4.17.3 -markdown-it-py==2.2.0 -markupsafe==2.1.2 +jsonschema==4.18.0 +jsonschema-specifications==2023.6.1 +markdown-it-py==3.0.0 +markupsafe==2.1.3 mdurl==0.1.2 packaging==23.1 -pluggy==1.0.0 +pluggy==1.2.0 pycparser==2.21 pygments==2.15.1 -pyrsistent==0.19.3 -python-dateutil==2.8.2 -python-slugify==8.0.1 pyyaml==6.0 -requests==2.31.0 -resolvelib==0.8.1 -rich==13.3.4 -six==1.16.0 +referencing==0.29.1 +resolvelib==1.0.1 +rich==13.4.2 +rpds-py==0.8.10 subprocess-tee==0.4.1 -text-unidecode==1.3 -urllib3==1.26.15 +typing-extensions==4.7.1 diff --git a/.config/requirements.in b/.config/requirements.in index cdf55fa29e..bdcd7a7e6d 100644 --- a/.config/requirements.in +++ b/.config/requirements.in @@ -2,7 +2,6 @@ ansible-compat >= 4.1.2 ansible-core >= 2.12.10 click >= 8.0, < 9 click-help-colors >= 0.9 -cookiecutter >= 1.7.3 # dependency issues in older versions enrich >= 1.2.7 jsonschema >= 4.9.1 Jinja2 >= 2.11.3 diff --git a/.config/requirements.txt b/.config/requirements.txt index b240076866..e7c57e4231 100644 --- a/.config/requirements.txt +++ b/.config/requirements.txt @@ -6,105 +6,100 @@ # ansi2html==1.8.0 ansible-compat==4.1.2 -ansible-core==2.14.4 -ansible-lint==6.14.6 -arrow==1.2.3 +ansible-core==2.15.1 +ansible-lint==6.17.2 attrs==23.1.0 -beautifulsoup4==4.12.1 -binaryornot==0.4.4 +beautifulsoup4==4.12.2 black==23.3.0 bracex==2.3.post1 -cairocffi==1.5.0 +cairocffi==1.5.1 cairosvg==2.7.0 -certifi==2022.12.7 +certifi==2023.5.7 cffi==1.15.1 -chardet==5.1.0 charset-normalizer==3.1.0 -check-jsonschema==0.22.0 +check-jsonschema==0.23.2 click==8.1.3 click-help-colors==0.9.1 colorama==0.4.6 -cookiecutter==2.1.1 -coverage==7.2.3 -cryptography==40.0.2 +coverage==7.2.7 +cryptography==41.0.1 csscompressor==0.9.5 cssselect2==0.7.0 defusedxml==0.7.1 dnspython==2.3.0 enrich==1.2.7 -exceptiongroup==1.1.1 -execnet==1.9.0 -filelock==3.12.0 +exceptiongroup==1.1.2 +execnet==2.0.0 +filelock==3.12.2 ghp-import==2.1.0 -griffe==0.26.0 +griffe==0.29.0 htmlmin2==0.1.13 idna==3.4 -importlib-metadata==6.1.0 +importlib-metadata==6.6.0 +importlib-resources==5.0.7 iniconfig==2.0.0 jinja2==3.1.2 -jinja2-time==0.2.0 jsmin==3.0.1 -jsonschema==4.17.3 +jsonschema==4.18.0 +jsonschema-specifications==2023.6.1 linkchecker==10.2.1 markdown==3.3.7 -markdown-exec==1.4.0 +markdown-exec==1.6.0 markdown-include==0.8.1 -markdown-it-py==2.2.0 +markdown-it-py==3.0.0 markupsafe==2.1.2 mdurl==0.1.2 mergedeep==1.3.4 -mkdocs==1.4.2 -mkdocs-ansible==0.1.4 +mkdocs==1.4.3 +mkdocs-ansible==0.1.6 mkdocs-autorefs==0.4.1 -mkdocs-gen-files==0.4.0 -mkdocs-htmlproofer-plugin==0.11.0 -mkdocs-material==9.1.5 +mkdocs-gen-files==0.5.0 +mkdocs-htmlproofer-plugin==0.13.1 +mkdocs-material==9.1.15 mkdocs-material-extensions==1.1.1 mkdocs-minify-plugin==0.6.4 -mkdocs-monorepo-plugin==1.0.4 -mkdocstrings==0.21.2 -mkdocstrings-python==0.9.0 +mkdocs-monorepo-plugin==1.0.5 +mkdocstrings==0.22.0 +mkdocstrings-python==1.1.0 mypy-extensions==1.0.0 -packaging==23.0 +packaging==23.1 pathspec==0.11.1 pexpect==4.8.0 pillow==9.5.0 -pipdeptree==2.7.0 -platformdirs==3.2.0 -pluggy==1.0.0 +pipdeptree==2.7.1 +platformdirs==3.8.1 +pluggy==1.2.0 ptyprocess==0.7.0 pycparser==2.21 -pygments==2.14.0 -pymdown-extensions==9.10 -pyrsistent==0.19.3 -pytest==7.3.1 -pytest-mock==3.10.0 +pygments==2.15.1 +pymdown-extensions==10.0.1 +pytest==7.4.0 +pytest-mock==3.11.1 pytest-plus==0.4.0 -pytest-testinfra==7.0.0 -pytest-xdist==3.2.1 +pytest-testinfra==8.1.0 +pytest-xdist==3.3.1 python-dateutil==2.8.2 python-slugify==8.0.1 pyyaml==6.0 pyyaml-env-tag==0.1 -regex==2023.3.23 -requests==2.28.2 -resolvelib==0.8.1 -rich==13.3.4 -ruamel-yaml==0.17.21 +referencing==0.29.1 +regex==2023.5.5 +requests==2.31.0 +resolvelib==1.0.1 +rich==13.4.2 +rpds-py==0.8.10 +ruamel-yaml==0.17.31 ruamel-yaml-clib==0.2.7 six==1.16.0 -soupsieve==2.4 +soupsieve==2.4.1 subprocess-tee==0.4.1 text-unidecode==1.3 tinycss2==1.2.1 tomli==2.0.1 -typing-extensions==4.5.0 -urllib3==1.26.15 +typing-extensions==4.6.2 +urllib3==2.0.2 watchdog==3.0.0 wcmatch==8.4.1 webencodings==0.5.1 -yamllint==1.30.0 +yamllint==1.32.0 zipp==3.15.0 - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 89acb6c651..9367ea7de9 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -51,7 +51,7 @@ jobs: matrix: ${{ fromJson(needs.pre.outputs.matrix) }} env: - PYTEST_REQPASS: 450 + PYTEST_REQPASS: 445 steps: - uses: actions/checkout@v3 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e44277685e..ffee93fb33 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,6 @@ repos: # Temporary excludes so we can gradually normalize the formatting exclude: > (?x)^( - src/molecule/cookiecutter/.*| src/molecule/test/resources/templates/.*| )$ additional_dependencies: @@ -63,7 +62,7 @@ repos: pass_filenames: false additional_dependencies: - ansible-compat>=4.1.2 - - click>=8.0.1 + - click>=8.0.1,<8.1.4 # https://github.com/pallets/click/issues/2558 - enrich>=1.2.7 - importlib-metadata>=4.6.1 - jinja2 @@ -87,7 +86,6 @@ repos: - ansible-compat>=4.1.2 - click - click-help-colors - - cookiecutter - enrich>=1.2.7 - filelock - jsonschema diff --git a/.yamllint b/.yamllint index 048a44c537..097600ce91 100644 --- a/.yamllint +++ b/.yamllint @@ -1,10 +1,6 @@ --- # Based on ansible-lint config extends: default -ignore: | - src/molecule/cookiecutter/scenario/driver/delegated/{{cookiecutter.molecule_directory}} - src/molecule/cookiecutter/molecule/{{cookiecutter.role_name}} - src/molecule/cookiecutter/scenario/verifier/ansible/{{cookiecutter.molecule_directory}} rules: braces: max-spaces-inside: 1 diff --git a/docs/examples.md b/docs/examples.md index 708744341e..4a86f1ba7c 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -22,7 +22,6 @@ items: use as-is[^1]. > - When `true`, use the specified `platform[].image` as-is. - > > - When `false`, exec `docker build` to customize base image > using either: > diff --git a/docs/getting-started.md b/docs/getting-started.md index acfc73313c..7198dd633d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -11,29 +11,6 @@ familiar with the basics of how to use Molecule and what it can offer. external Python dependency for the Docker driver which is provided when installing Molecule using `pip install 'molecule-plugins[docker]'`. -## Creating a new role - -Molecule uses [ansible-galaxy](https://docs.ansible.com/ansible/latest/cli/ansible-galaxy.html) under the hood to generate conventional role layouts. If -you've ever worked with Ansible roles before, you'll be right at home. -If not, please review the -[role directory structure](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html#role-directory-structure) -guide to see what each folder is responsible for. - -To generate a new role with Molecule, simply run: - -```shell -$ molecule init role acme.my_new_role --driver-name docker -``` - -You should then see a `my_new_role` folder in your current directory. - -!!! note - - For future reference, if you want to initialize Molecule within an - existing role, you would use the - `molecule init scenario -r my_role_name` command from within the role's - directory (e.g. `my_role_name/`). - ## Molecule Scenarios You will notice one new folder which is the `molecule` folder. diff --git a/docs/usage.md b/docs/usage.md index f3ff0bfa10..79e1d42c77 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -45,12 +45,6 @@ ## molecule init -### molecule init role - -Initialize a new role using ansible-galaxy and include default -molecule directory. Please refer to the `init scenario` -command in order to generate a custom `molecule` scenario. - ### molecule init scenario ## molecule list diff --git a/pyproject.toml b/pyproject.toml index 93d2023b14..b65a9cc9fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,6 @@ no_incremental = true [[tool.mypy.overrides]] module = [ "click_help_colors", # https://github.com/click-contrib/click-help-colors/issues/20 - "cookiecutter.*", # https://github.com/cookiecutter/cookiecutter/issues/1878 "pexpect", # https://github.com/pexpect/pexpect/issues/759 "pluggy", # https://github.com/pytest-dev/pluggy/pull/414 "testinfra.*", # https://github.com/pytest-dev/pytest-testinfra/issues/619 @@ -158,7 +157,6 @@ norecursedirs = [ "build", "dist", "doc", - "src/molecule/cookiecutter/scenario", "src/molecule/test/resources", "src/molecule/test/scenarios", ] diff --git a/src/molecule/command/cleanup.py b/src/molecule/command/cleanup.py index 85b5da74ea..84dc19ceb8 100644 --- a/src/molecule/command/cleanup.py +++ b/src/molecule/command/cleanup.py @@ -53,7 +53,7 @@ def execute(self, action_args=None): default=base.MOLECULE_DEFAULT_SCENARIO_NAME, help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})", ) -def cleanup(ctx, scenario_name): # pragma: no cover +def cleanup(ctx, scenario_name="default"): # pragma: no cover """Use the provisioner to cleanup any changes made to external systems during \ the stages of testing. """ diff --git a/src/molecule/command/init/base.py b/src/molecule/command/init/base.py index 2bee237401..d3dfdb8d7a 100644 --- a/src/molecule/command/init/base.py +++ b/src/molecule/command/init/base.py @@ -23,9 +23,6 @@ import logging import os -import cookiecutter -import cookiecutter.main - from molecule import util LOG = logging.getLogger(__name__) @@ -36,58 +33,6 @@ class Base: __metaclass__ = abc.ABCMeta - def _process_templates( - self, - template_dir, - extra_context, - output_dir, - overwrite=True, - ): - """Process templates as found in the named directory. - - :param template_dir: A string containing an absolute or relative path - to a directory where the templates are located. If the provided - directory is a relative path, it is resolved using a known location. - :param extra_context: A dict of values that are used to override - default or user specified values. - :param output_dir: An string with an absolute path to a directory where - the templates should be written to. - :param overwrite: An optional bool whether or not to overwrite existing - templates. - :return: None - """ - template_dir = self._resolve_template_dir(template_dir) - self._validate_template_dir(template_dir) - - try: - cookiecutter.main.cookiecutter( - template_dir, - extra_context=extra_context, - output_dir=output_dir, - overwrite_if_exists=overwrite, - no_input=True, - ) - except cookiecutter.exceptions.NonTemplatedInputDirException: - util.sysexit_with_message( - "The specified template directory (" - + str(template_dir) - + ") is in an invalid format", - ) - - def _resolve_template_dir(self, template_dir): - if not os.path.isabs(template_dir): - template_dir = os.path.abspath( - os.path.join( - os.path.dirname(__file__), - os.path.pardir, - os.path.pardir, - "cookiecutter", - template_dir, - ), - ) - - return template_dir - def _validate_template_dir(self, template_dir): if not os.path.isdir(template_dir): util.sysexit_with_message( diff --git a/src/molecule/command/init/init.py b/src/molecule/command/init/init.py index eb53deb4f3..d22d20f029 100644 --- a/src/molecule/command/init/init.py +++ b/src/molecule/command/init/init.py @@ -22,7 +22,7 @@ import logging from molecule.command import base -from molecule.command.init import role, scenario +from molecule.command.init import scenario LOG = logging.getLogger(__name__) @@ -32,5 +32,4 @@ def init(): # pragma: no cover """Initialize a new role or scenario.""" -init.add_command(role.role) init.add_command(scenario.scenario) diff --git a/src/molecule/command/init/role.py b/src/molecule/command/init/role.py deleted file mode 100644 index 7f311e0622..0000000000 --- a/src/molecule/command/init/role.py +++ /dev/null @@ -1,185 +0,0 @@ -# Copyright (c) 2015-2018 Cisco Systems, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -"""Base class used by init role command.""" - -import logging -import os -import re - -import click - -from molecule import api, util -from molecule.command import base as command_base -from molecule.command.init import base -from molecule.config import DEFAULT_DRIVER - -LOG = logging.getLogger(__name__) - - -class Role(base.Base): - """Init Role Command Class. - - .. program:: molecule init role acme.foo - - .. option:: molecule init role acme.foo - - Initialize a new role. - - Initialize a new role using ansible-galaxy and include default - molecule directory. Please refer to the ``init scenario`` - command in order to generate a custom ``molecule`` scenario. - """ - - def __init__(self, command_args) -> None: - """Construct Role.""" - self._command_args = command_args - - def execute(self, action_args=None): - """Execute the actions necessary to perform a `molecule init role` and \ - returns None. - - :return: None - """ - namespace = None - role_name = self._command_args["role_name"] - role_directory = os.getcwd() - - # outside collections our tooling needs a namespace. - if not os.path.isfile("../galaxy.yml"): - name_re = re.compile(r"^[a-z][a-z0-9_]+\.[a-z][a-z0-9_]+$") - - if not name_re.match(role_name): - util.sysexit_with_message( - "Outside collections you must mention role " - "namespace like: molecule init role 'acme.myrole'. Be sure " - "you use only lowercase characters and underlines. See https://galaxy.ansible.com/docs/contributing/creating_role.html", - ) - namespace, role_name = role_name.split(".") - - msg = f"Initializing new role {role_name}..." - LOG.info(msg) - - if os.path.isdir(role_name): - msg = f"The directory {role_name} exists. Cannot create new role." - util.sysexit_with_message(msg) - - cmd = ["ansible-galaxy", "init", "-v", "--offline", role_name] - result = util.run_command(cmd) - - if result.returncode != 0: - util.sysexit_with_message( - f"Galaxy failed to create role, returned {result.returncode!s}", - ) - - if namespace: - # we need to inject namespace info into meta/main.yml - cmd_meta = [ - "ansible", - "localhost", - "-o", # one line output - "-m", - "lineinfile", - "-a", - f'path={role_name}/meta/main.yml line=" namespace: {namespace}" insertafter=" author: your name"', - ] - util.run_command(cmd_meta, check=True) - # we need to inject namespace info into tests/test.yml - cmd_tests = [ - "ansible", - "localhost", - "-o", # one line output - "-m", - "lineinfile", - "-a", - f'path={role_name}/tests/test.yml line=" - {namespace}.{role_name}" regex="^(.*) - {role_name}"', - ] - util.run_command(cmd_tests, check=True) - - scenario_base_directory = os.path.join(role_directory, role_name) - templates = [ - api.drivers()[self._command_args["driver_name"]].template_dir(), - api.verifiers()[self._command_args["verifier_name"]].template_dir(), - ] - self._process_templates( - "molecule", - {**self._command_args, "role_name": role_name}, - role_directory, - ) - for template in templates: - self._process_templates( - template, - self._command_args, - scenario_base_directory, - ) - - role_directory = os.path.join(role_directory, role_name) - msg = f"Initialized role in {role_directory} successfully." - LOG.info(msg) - - -@command_base.click_command_ex() -@click.pass_context -@click.option( - "--dependency-name", - type=click.Choice(["galaxy"]), - default="galaxy", - help="Name of dependency to initialize. (galaxy)", -) -@click.option( - "--driver-name", - "-d", - type=click.Choice([str(s) for s in api.drivers()]), - default=DEFAULT_DRIVER, - help=f"Name of driver to initialize. ({DEFAULT_DRIVER})", -) -@click.option( - "--provisioner-name", - type=click.Choice(["ansible"]), - default="ansible", - help="Name of provisioner to initialize. (ansible)", -) -@click.argument("ROLE-NAME", required=True) -@click.option( - "--verifier-name", - type=click.Choice([str(s) for s in api.verifiers()]), - default="ansible", - help="Name of verifier to initialize. (ansible)", -) -def role( - ctx, - dependency_name, - driver_name, - provisioner_name, - role_name, - verifier_name, -): # pragma: no cover - """Initialize a new role for use with Molecule, namespace is required outside collections, like acme.myrole.""" - command_args = { - "dependency_name": dependency_name, - "driver_name": driver_name, - "provisioner_name": provisioner_name, - "role_name": role_name, - "scenario_name": command_base.MOLECULE_DEFAULT_SCENARIO_NAME, - "subcommand": __name__, - "verifier_name": verifier_name, - } - - r = Role(command_args) - r.execute() diff --git a/src/molecule/command/init/scenario.py b/src/molecule/command/init/scenario.py index 529f074f03..6ac651d632 100644 --- a/src/molecule/command/init/scenario.py +++ b/src/molecule/command/init/scenario.py @@ -19,6 +19,7 @@ # DEALINGS IN THE SOFTWARE. """Base class used by init scenario command.""" +import json import logging import os @@ -27,7 +28,7 @@ from molecule import api, config, util from molecule.command import base as command_base from molecule.command.init import base -from molecule.config import DEFAULT_DRIVER +from molecule.config import DEFAULT_DRIVER, MOLECULE_EMBEDDED_DATA_DIR LOG = logging.getLogger(__name__) @@ -53,8 +54,7 @@ class Scenario(base.Base): .. option:: cd foo; molecule init scenario bar --role-name foo - Initialize a new scenario using a local *cookiecutter* template for the - driver configuration. + Initialize a new scenario using a embedded template. """ def __init__(self, command_args: dict[str, str]) -> None: @@ -68,14 +68,10 @@ def execute(self, action_args=None): :return: None """ scenario_name = self._command_args["scenario_name"] - role_name = os.getcwd().split(os.sep)[-1] - role_directory = util.abs_path(os.path.join(os.getcwd(), os.pardir)) msg = f"Initializing new scenario {scenario_name}..." LOG.info(msg) - molecule_directory = config.molecule_directory( - os.path.join(role_directory, role_name), - ) + molecule_directory = config.molecule_directory(os.getcwd()) scenario_directory = os.path.join(molecule_directory, scenario_name) if os.path.isdir(scenario_directory): @@ -85,34 +81,23 @@ def execute(self, action_args=None): ) util.sysexit_with_message(msg) - driver_template = api.drivers()[ - self._command_args["driver_name"] - ].template_dir() - if "driver_template" in self._command_args: - self._validate_template_dir(self._command_args["driver_template"]) - cli_driver_template = f"{self._command_args['driver_template']}/{self._command_args['driver_name']}" - if os.path.isdir(cli_driver_template): - driver_template = cli_driver_template - else: - LOG.warning( - "Driver not found in custom template directory(%s), " - "using the default template instead", - cli_driver_template, - ) - scenario_base_directory = os.path.join(role_directory, role_name) - templates = [ - driver_template, - api.verifiers()[self._command_args["verifier_name"]].template_dir(), + extra_vars = json.dumps(self._command_args) + cmd = [ + "ansible-playbook", + "-i", + "localhost,", + "--connection=local", + "--extra-vars", + extra_vars, + f"{MOLECULE_EMBEDDED_DATA_DIR}/init-scenario.yml", ] - self._process_templates("molecule", self._command_args, role_directory) - for template in templates: - self._process_templates( - template, - self._command_args, - scenario_base_directory, - ) + env = os.environ.copy() + # As ansible fails to find a terminal when run by molecule, we force + # it to use colors. + env["ANSIBLE_FORCE_COLOR"] = "1" + env["ANSIBLE_PYTHON_INTERPRETER"] = "auto_silent" + util.run_command(cmd, env=env, check=True) - role_directory = os.path.join(role_directory, role_name) msg = f"Initialized scenario in {scenario_directory} successfully." LOG.info(msg) @@ -130,20 +115,6 @@ def _role_exists(ctx, param, value: str): # pragma: no cover return value -def _default_scenario_exists(ctx, param, value: str): # pragma: no cover - if value == command_base.MOLECULE_DEFAULT_SCENARIO_NAME: - return value - - default_scenario_directory = os.path.join( - "molecule", - command_base.MOLECULE_DEFAULT_SCENARIO_NAME, - ) - if not os.path.exists(default_scenario_directory): - msg = f"The default scenario not found. Please create a scenario named '{command_base.MOLECULE_DEFAULT_SCENARIO_NAME}' first." - util.sysexit_with_message(msg) - return value - - @command_base.click_command_ex() @click.pass_context @click.option( @@ -165,33 +136,17 @@ def _default_scenario_exists(ctx, param, value: str): # pragma: no cover default="ansible", help="Name of provisioner to initialize. (ansible)", ) -@click.option( - "--role-name", - "-r", - required=False, - callback=_role_exists, - help="Name of the role to create.", -) @click.argument( "scenario-name", default=command_base.MOLECULE_DEFAULT_SCENARIO_NAME, required=False, - callback=_default_scenario_exists, -) -@click.option( - "--verifier-name", - type=click.Choice([str(s) for s in api.verifiers()]), - default="ansible", - help="Name of verifier to initialize. (ansible)", ) def scenario( ctx, dependency_name, driver_name, provisioner_name, - role_name, scenario_name, - verifier_name, ): # pragma: no cover """Initialize a new scenario for use with Molecule. @@ -201,10 +156,8 @@ def scenario( "dependency_name": dependency_name, "driver_name": driver_name, "provisioner_name": provisioner_name, - "role_name": role_name, "scenario_name": scenario_name, "subcommand": __name__, - "verifier_name": verifier_name, } s = Scenario(command_args) diff --git a/src/molecule/command/verify.py b/src/molecule/command/verify.py index b06c078cc4..69a6db92f8 100644 --- a/src/molecule/command/verify.py +++ b/src/molecule/command/verify.py @@ -48,7 +48,7 @@ def execute(self, action_args=None): default=base.MOLECULE_DEFAULT_SCENARIO_NAME, help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})", ) -def verify(ctx, scenario_name): # pragma: no cover +def verify(ctx, scenario_name="default"): # pragma: no cover """Run automated tests against instances.""" args = ctx.obj.get("args") subcommand = base._get_subcommand(__name__) diff --git a/src/molecule/config.py b/src/molecule/config.py index 826d726bd9..e0aca4ba42 100644 --- a/src/molecule/config.py +++ b/src/molecule/config.py @@ -31,6 +31,7 @@ from molecule import api, interpolation, platforms, scenario, state, util from molecule.app import app +from molecule.data import __file__ as data_module from molecule.dependency import ansible_galaxy, shell from molecule.model import schema_v3 from molecule.provisioner import ansible @@ -44,6 +45,8 @@ MOLECULE_KEEP_STRING = "MOLECULE_" DEFAULT_DRIVER = "delegated" +MOLECULE_EMBEDDED_DATA_DIR = os.path.dirname(data_module) + @cache def ansible_version() -> Version: diff --git a/src/molecule/cookiecutter/molecule/cookiecutter.json b/src/molecule/cookiecutter/molecule/cookiecutter.json deleted file mode 100644 index 5b1803c0e0..0000000000 --- a/src/molecule/cookiecutter/molecule/cookiecutter.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "molecule_directory": "molecule", - "dependency_name": "OVERRIDDEN", - "driver_name": "OVERRIDDEN", - "provisioner_name": "OVERRIDDEN", - "scenario_name": "OVERRIDDEN", - "role_name": "OVERRIDDEN", - "verifier_name": "OVERRIDDEN" -} diff --git a/src/molecule/cookiecutter/molecule/{{cookiecutter.role_name}}/.yamllint b/src/molecule/cookiecutter/molecule/{{cookiecutter.role_name}}/.yamllint deleted file mode 100644 index 8827676056..0000000000 --- a/src/molecule/cookiecutter/molecule/{{cookiecutter.role_name}}/.yamllint +++ /dev/null @@ -1,33 +0,0 @@ ---- -# Based on ansible-lint config -extends: default - -rules: - braces: - max-spaces-inside: 1 - level: error - brackets: - max-spaces-inside: 1 - level: error - colons: - max-spaces-after: -1 - level: error - commas: - max-spaces-after: -1 - level: error - comments: disable - comments-indentation: disable - document-start: disable - empty-lines: - max: 3 - level: error - hyphens: - level: error - indentation: disable - key-duplicates: enable - line-length: disable - new-line-at-end-of-file: disable - new-lines: - type: unix - trailing-spaces: disable - truthy: disable diff --git a/src/molecule/cookiecutter/molecule/{{cookiecutter.role_name}}/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/molecule.yml b/src/molecule/cookiecutter/molecule/{{cookiecutter.role_name}}/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/molecule.yml deleted file mode 100644 index 4cf31a06fb..0000000000 --- a/src/molecule/cookiecutter/molecule/{{cookiecutter.role_name}}/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/molecule.yml +++ /dev/null @@ -1,15 +0,0 @@ ---- -dependency: - name: {{ cookiecutter.dependency_name }} -driver: - name: {{ cookiecutter.driver_name }} -platforms: - - name: instance -{%- if cookiecutter.driver_name in ['containers', 'docker', 'podman'] %} - image: quay.io/centos/centos:stream8 - pre_build_image: true -{%- endif %} -provisioner: - name: {{ cookiecutter.provisioner_name }} -verifier: - name: {{ cookiecutter.verifier_name }} diff --git a/src/molecule/cookiecutter/scenario/driver/delegated/cookiecutter.json b/src/molecule/cookiecutter/scenario/driver/delegated/cookiecutter.json deleted file mode 100644 index 29f6b71802..0000000000 --- a/src/molecule/cookiecutter/scenario/driver/delegated/cookiecutter.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "molecule_directory": "molecule", - "role_name": "OVERRIDDEN", - "scenario_name": "OVERRIDDEN" -} diff --git a/src/molecule/cookiecutter/scenario/driver/delegated/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/INSTALL.rst b/src/molecule/cookiecutter/scenario/driver/delegated/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/INSTALL.rst deleted file mode 100644 index 1b38d098df..0000000000 --- a/src/molecule/cookiecutter/scenario/driver/delegated/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/INSTALL.rst +++ /dev/null @@ -1,15 +0,0 @@ -*********************************** -Delegated driver installation guide -*********************************** - -Requirements -============ - -This driver is delegated to the developer. Up to the developer to implement -requirements. - -Install -======= - -This driver is delegated to the developer. Up to the developer to implement -requirements. diff --git a/src/molecule/cookiecutter/scenario/driver/delegated/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml b/src/molecule/cookiecutter/scenario/driver/delegated/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml deleted file mode 100644 index aacb04462d..0000000000 --- a/src/molecule/cookiecutter/scenario/driver/delegated/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -- name: Converge - hosts: all - gather_facts: false - tasks: - - name: "Include {{ cookiecutter.role_name }}" - ansible.builtin.include_role: - name: "{{ cookiecutter.role_name }}" diff --git a/src/molecule/cookiecutter/scenario/verifier/ansible/cookiecutter.json b/src/molecule/cookiecutter/scenario/verifier/ansible/cookiecutter.json deleted file mode 100644 index bdad55b95a..0000000000 --- a/src/molecule/cookiecutter/scenario/verifier/ansible/cookiecutter.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "molecule_directory": "molecule", - - "role_name": "OVERRIDDEN", - "scenario_name": "default" -} diff --git a/src/molecule/cookiecutter/scenario/verifier/ansible/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/verify.yml b/src/molecule/cookiecutter/scenario/verifier/ansible/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/verify.yml deleted file mode 100644 index ab2e9bca69..0000000000 --- a/src/molecule/cookiecutter/scenario/verifier/ansible/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/verify.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -# This is an example playbook to execute Ansible tests. - -{% raw -%} -- name: Verify - hosts: all - gather_facts: false - tasks: - - name: Example assertion - ansible.builtin.assert: - that: true -{% endraw -%} diff --git a/src/molecule/cookiecutter/scenario/verifier/testinfra/cookiecutter.json b/src/molecule/cookiecutter/scenario/verifier/testinfra/cookiecutter.json deleted file mode 100644 index 0201530cfc..0000000000 --- a/src/molecule/cookiecutter/scenario/verifier/testinfra/cookiecutter.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "molecule_directory": "molecule", - - "role_name": "OVERRIDDEN", - "scenario_name": "default", - "verifier_directory": "tests" -} diff --git a/src/molecule/cookiecutter/scenario/verifier/testinfra/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/{{cookiecutter.verifier_directory}}/conftest.py b/src/molecule/cookiecutter/scenario/verifier/testinfra/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/{{cookiecutter.verifier_directory}}/conftest.py deleted file mode 100644 index a11928cb29..0000000000 --- a/src/molecule/cookiecutter/scenario/verifier/testinfra/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/{{cookiecutter.verifier_directory}}/conftest.py +++ /dev/null @@ -1,22 +0,0 @@ -"""PyTest Fixtures.""" - -import os - -import pytest - - -def pytest_runtest_setup(item): - """Run tests only when under molecule with testinfra installed.""" - try: - import testinfra - except ImportError: - pytest.skip("Test requires testinfra", allow_module_level=True) - if "MOLECULE_INVENTORY_FILE" in os.environ: - pytest.testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( - os.environ["MOLECULE_INVENTORY_FILE"], - ).get_hosts("all") - else: - pytest.skip( - "Test should run only from inside molecule.", - allow_module_level=True, - ) diff --git a/src/molecule/cookiecutter/scenario/verifier/testinfra/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/{{cookiecutter.verifier_directory}}/test_default.py b/src/molecule/cookiecutter/scenario/verifier/testinfra/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/{{cookiecutter.verifier_directory}}/test_default.py deleted file mode 100644 index 0cff6693ff..0000000000 --- a/src/molecule/cookiecutter/scenario/verifier/testinfra/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/{{cookiecutter.verifier_directory}}/test_default.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Role testing files using testinfra.""" - - -def test_hosts_file(host): - """Validate /etc/hosts file.""" - f = host.file("/etc/hosts") - - assert f.exists - assert f.user == "root" - assert f.group == "root" diff --git a/src/molecule/data/init-scenario.yml b/src/molecule/data/init-scenario.yml new file mode 100644 index 0000000000..d5bd56ef19 --- /dev/null +++ b/src/molecule/data/init-scenario.yml @@ -0,0 +1,33 @@ +- name: Create a new molecule scenario + hosts: localhost + gather_facts: false + vars_prompt: + - name: scenario_name + prompt: What is the scenario name? + vars: + dest: "{{ ('molecule/' + (scenario_name | default('default'))) | realpath }}" + tasks: + - name: Check if destination folder exists + ansible.builtin.file: + path: "{{ dest }}" + state: directory + mode: "0700" + - name: Check if destination folder is empty + ansible.builtin.find: + paths: "{{ dest }}" + register: dest_content + - name: Fail if destination folder is not empty + when: dest_content.matched > 0 + ansible.builtin.fail: + msg: Refused to expand templates as destination folder '{{ dest }}' as it already has content in it. + - name: Expand templates + vars: + dest_file: "{{ dest }}/{{ item | basename | regex_replace('\\.j2$', '') }}" + ansible.builtin.template: + src: "{{ item }}" + dest: "{{ dest_file }}" + mode: "0644" + with_fileglob: + - templates/scenario/*.j2 + loop_control: + label: "{{ dest_file | relpath }}" diff --git a/src/molecule/data/templates/scenario/converge.yml.j2 b/src/molecule/data/templates/scenario/converge.yml.j2 new file mode 100644 index 0000000000..e60075622d --- /dev/null +++ b/src/molecule/data/templates/scenario/converge.yml.j2 @@ -0,0 +1,8 @@ +--- +- name: Converge + hosts: all + gather_facts: false + tasks: + - name: Replace this task with one that validates your content + ansible.builtin.debug: + msg: "This is the effective test" diff --git a/src/molecule/cookiecutter/scenario/driver/delegated/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/create.yml b/src/molecule/data/templates/scenario/create.yml.j2 similarity index 100% rename from src/molecule/cookiecutter/scenario/driver/delegated/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/create.yml rename to src/molecule/data/templates/scenario/create.yml.j2 diff --git a/src/molecule/cookiecutter/scenario/driver/delegated/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/destroy.yml b/src/molecule/data/templates/scenario/destroy.yml.j2 similarity index 100% rename from src/molecule/cookiecutter/scenario/driver/delegated/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/destroy.yml rename to src/molecule/data/templates/scenario/destroy.yml.j2 diff --git a/src/molecule/data/templates/scenario/molecule.yml.j2 b/src/molecule/data/templates/scenario/molecule.yml.j2 new file mode 100644 index 0000000000..b524ee7450 --- /dev/null +++ b/src/molecule/data/templates/scenario/molecule.yml.j2 @@ -0,0 +1,6 @@ +--- +platforms: + - name: instance + # you might want to add your own variables here based on what provisioning + # you are doing like: + # image: quay.io/centos/centos:stream8 diff --git a/src/molecule/driver/base.py b/src/molecule/driver/base.py index 487e3d8a56..2e5307a6be 100644 --- a/src/molecule/driver/base.py +++ b/src/molecule/driver/base.py @@ -24,7 +24,6 @@ from abc import ABCMeta, abstractmethod from importlib.metadata import version -import molecule from molecule.status import Status @@ -240,21 +239,6 @@ def __rich__(self): """Return rich representation of object.""" return self.__str__() - def template_dir(self): - p = os.path.join(self._path, "cookiecutter") - - if not os.path.isdir(p): - p = os.path.abspath( - os.path.join( - os.path.dirname(molecule.__file__), - "cookiecutter", - "scenario", - "driver", - self.name, - ), - ) - return p - def get_playbook(self, step): """Return embedded playbook location or None. diff --git a/src/molecule/test/a_unit/command/init/test_role.py b/src/molecule/test/a_unit/command/init/test_role.py deleted file mode 100644 index 825fbf8705..0000000000 --- a/src/molecule/test/a_unit/command/init/test_role.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) 2015-2018 Cisco Systems, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import os - -import pytest - -from molecule.command.init import role - - -@pytest.fixture() -def _command_args(): - return { - "dependency_name": "galaxy", - "driver_name": "delegated", - "provisioner_name": "ansible", - "role_name": "acme.test_role", - "scenario_name": "default", - "subcommand": __name__, - "verifier_name": "ansible", - } - - -@pytest.fixture() -def _instance(_command_args): - return role.Role(_command_args) - - -def test_execute(temp_dir, _instance, patched_logger_info): - _instance.execute() - - msg = "Initializing new role test_role..." - patched_logger_info.assert_any_call(msg) - - assert os.path.isdir("./test_role") - assert os.path.isdir("./test_role/molecule/default") - - role_directory = os.path.join(temp_dir.strpath, "test_role") - msg = f"Initialized role in {role_directory} successfully." - patched_logger_info.assert_any_call(msg) - - -def test_execute_role_exists(temp_dir, _instance, caplog): - _instance.execute() - - with pytest.raises(SystemExit) as e: - _instance.execute() - - assert e.value.code == 1 - - msg = "The directory test_role exists. Cannot create new role." - assert msg in caplog.text diff --git a/src/molecule/test/a_unit/command/init/test_scenario.py b/src/molecule/test/a_unit/command/init/test_scenario.py index a654dcfd36..086d65902b 100644 --- a/src/molecule/test/a_unit/command/init/test_scenario.py +++ b/src/molecule/test/a_unit/command/init/test_scenario.py @@ -41,15 +41,6 @@ def _instance(_command_args): return scenario.Scenario(_command_args) -@pytest.fixture() -def invalid_template_dir(resources_folder_path): - invalid_role_template_path = os.path.join( - resources_folder_path, - "invalid_scenario_template", - ) - return invalid_role_template_path - - def test_execute(temp_dir, _instance, patched_logger_info): _instance.execute() @@ -73,15 +64,3 @@ def test_execute_scenario_exists(temp_dir, _instance, caplog): msg = "The directory molecule/test-scenario exists. Cannot create new scenario." assert msg in caplog.text - - -def test_execute_with_invalid_driver( - temp_dir, - _instance, - _command_args, - caplog, -): - _command_args["driver_name"] = "ec3" - - with pytest.raises(KeyError): - _instance.execute() diff --git a/src/molecule/test/a_unit/cookiecutter/test_molecule.py b/src/molecule/test/a_unit/cookiecutter/test_molecule.py index 2d6625ad45..fc9cae67b0 100644 --- a/src/molecule/test/a_unit/cookiecutter/test_molecule.py +++ b/src/molecule/test/a_unit/cookiecutter/test_molecule.py @@ -22,9 +22,7 @@ import pytest -from molecule import util from molecule.command.init import base -from molecule.model import schema_v3 class CommandBase(base.Base): @@ -67,15 +65,3 @@ def _molecule_file(_role_directory): "default", "molecule.yml", ) - - -def test_valid(temp_dir, _molecule_file, _role_directory, _command_args, _instance): - _instance._process_templates("molecule", _command_args, _role_directory) - - data = util.safe_load_file(_molecule_file) - - assert not schema_v3.validate(data) - - cmd = ["yamllint", "-s", _molecule_file] - result = util.run_command(cmd) - assert result.returncode == 0 diff --git a/src/molecule/test/b_functional/conftest.py b/src/molecule/test/b_functional/conftest.py index d777a3d378..664463be0b 100644 --- a/src/molecule/test/b_functional/conftest.py +++ b/src/molecule/test/b_functional/conftest.py @@ -105,26 +105,8 @@ def idempotence(scenario_name): assert run_command(cmd).returncode == 0 -def init_role(temp_dir, driver_name): - role_directory = os.path.join(temp_dir.strpath, "myrole") - - cmd = ["molecule", "init", "role", "myorg.myrole", "--driver-name", driver_name] - assert run_command(cmd).returncode == 0 - metadata_lint_update(role_directory) - - with change_dir_to(role_directory): - cmd = ["molecule", "test", "--all"] - assert run_command(cmd).returncode == 0 - - def init_scenario(temp_dir, driver_name): - # Create role - role_directory = os.path.join(temp_dir.strpath, "test_init") - cmd = ["molecule", "init", "role", "myorg.test_init", "--driver-name", driver_name] - assert run_command(cmd).returncode == 0 - metadata_lint_update(role_directory) - - with change_dir_to(role_directory): + with change_dir_to(temp_dir.strpath): # Create scenario molecule_dir = molecule_directory() scenario_directory = os.path.join(molecule_dir, "test-scenario") @@ -134,8 +116,6 @@ def init_scenario(temp_dir, driver_name): "init", "scenario", "test-scenario", - "--role-name", - "test_init", "--driver-name", driver_name, ] diff --git a/src/molecule/test/b_functional/test_command.py b/src/molecule/test/b_functional/test_command.py index 082ec542f7..0772adef9b 100644 --- a/src/molecule/test/b_functional/test_command.py +++ b/src/molecule/test/b_functional/test_command.py @@ -26,7 +26,6 @@ from molecule.test.b_functional.conftest import ( idempotence, - init_role, init_scenario, list_with_format_plain, run_test, @@ -174,15 +173,8 @@ def test_command_idempotence(scenario_to_test, with_scenario, scenario_name): @pytest.mark.serial() -@pytest.mark.parametrize("driver_name", [("delegated")], indirect=["driver_name"]) -def test_command_init_role(temp_dir, driver_name): - init_role(temp_dir, driver_name) - - -@pytest.mark.serial() -@pytest.mark.parametrize("driver_name", [("delegated")], indirect=["driver_name"]) -def test_command_init_scenario(temp_dir, driver_name): - init_scenario(temp_dir, driver_name) +def test_command_init_scenario(temp_dir): + init_scenario(temp_dir, "delegated") @pytest.mark.serial() diff --git a/src/molecule/test/resources/templates/cookiecutter.json b/src/molecule/test/resources/templates/cookiecutter.json deleted file mode 100644 index 220e2b2e76..0000000000 --- a/src/molecule/test/resources/templates/cookiecutter.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "repo_name": "OVERRIDDEN", - "value": "foo" -} diff --git a/src/molecule/test/resources/templates/{{cookiecutter.repo_name}}/template.yml b/src/molecule/test/resources/templates/{{cookiecutter.repo_name}}/template.yml deleted file mode 100644 index face6afc74..0000000000 --- a/src/molecule/test/resources/templates/{{cookiecutter.repo_name}}/template.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -- value: {{ cookiecutter.value }} diff --git a/src/molecule/verifier/base.py b/src/molecule/verifier/base.py index a8be48d0ca..69a4c76f2b 100644 --- a/src/molecule/verifier/base.py +++ b/src/molecule/verifier/base.py @@ -22,7 +22,6 @@ import abc import os -import molecule from molecule import util @@ -121,15 +120,3 @@ def __str__(self) -> str: def __repr__(self) -> str: """Return detailed string representation of object.""" return self.name - - def template_dir(self): - p = os.path.abspath( - os.path.join( - os.path.dirname(molecule.__file__), - "cookiecutter", - "scenario", - "verifier", - self.name, - ), - ) - return p