Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions .config/constraints.txt
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
# This file was autogenerated by uv via the following command:
# tox run -e deps
ansi2html==1.9.2 # via molecule (pyproject.toml)
ansible-builder==3.1.0 # via ansible-navigator
ansible-compat==25.5.0 # via ansible-lint, molecule (pyproject.toml)
ansible-lint==25.5.0 # via molecule (pyproject.toml)
ansible-lint==25.5.0 # via ansible-navigator, molecule (pyproject.toml)
ansible-navigator==25.5.0 # via molecule (pyproject.toml)
ansible-runner==2.4.1 # via ansible-navigator
astroid==3.3.10 # via pylint
attrs==25.3.0 # via jsonschema, referencing
babel==2.17.0 # via mkdocs-material
backrefs==5.8 # via mkdocs-material
beautifulsoup4==4.13.4 # via linkchecker, mkdocs-htmlproofer-plugin
bindep==2.13.0 # via ansible-builder
black==25.1.0 # via ansible-lint
bracex==2.5.post1 # via wcmatch
cachetools==6.0.0 # via tox
cairocffi==1.7.1 # via cairosvg
cairosvg==2.7.1 # via mkdocs-ansible
certifi==2025.4.26 # via requests
cffi==1.17.1 # via cairocffi, cryptography
cffi==1.17.1 # via cairocffi, cryptography, onigurumacffi
cfgv==3.4.0 # via pre-commit
chardet==5.2.0 # via tox
charset-normalizer==3.4.2 # via requests
Expand All @@ -28,6 +32,7 @@ cssselect2==0.8.0 # via cairosvg
defusedxml==0.7.1 # via cairosvg
dill==0.4.0 # via pylint
distlib==0.3.9 # via virtualenv
distro==1.9.0 # via bindep
dnspython==2.7.0 # via linkchecker
docker==7.1.0 # via molecule (pyproject.toml)
docstring-parser-fork==0.0.12 # via pydoclint
Expand All @@ -44,11 +49,12 @@ idna==3.10 # via requests
importlib-metadata==8.7.0 # via ansible-lint
iniconfig==2.1.0 # via pytest
isort==6.0.1 # via pylint
jinja2==3.1.6 # via ansible-core, mkdocs, mkdocs-macros-plugin, mkdocs-material, mkdocstrings, molecule (pyproject.toml)
jinja2==3.1.6 # via ansible-core, ansible-navigator, mkdocs, mkdocs-macros-plugin, mkdocs-material, mkdocstrings, molecule (pyproject.toml)
jsmin==3.0.1 # via mkdocs-minify-plugin
jsonschema==4.24.0 # via ansible-compat, ansible-lint, molecule (pyproject.toml)
jsonschema-specifications==2025.4.1 # via jsonschema
linkchecker==10.5.0 # via mkdocs-ansible, molecule (pyproject.toml)
lockfile==0.12.2 # via python-daemon
markdown==3.8 # via markdown-include, mkdocs, mkdocs-autorefs, mkdocs-htmlproofer-plugin, mkdocs-material, mkdocstrings, pymdown-extensions
markdown-exec==1.10.3 # via mkdocs-ansible
markdown-include==0.8.1 # via mkdocs-ansible
Expand All @@ -72,10 +78,13 @@ mkdocstrings-python==1.16.11 # via mkdocs-ansible
mypy==1.16.0 # via molecule (pyproject.toml)
mypy-extensions==1.1.0 # via black, mypy
nodeenv==1.9.1 # via pre-commit
packaging==25.0 # via ansible-compat, ansible-core, ansible-lint, black, mkdocs, mkdocs-macros-plugin, pyproject-api, pytest, tox, molecule (pyproject.toml)
onigurumacffi==1.4.1 # via ansible-navigator
packaging==25.0 # via ansible-builder, ansible-compat, ansible-core, ansible-lint, ansible-runner, bindep, black, mkdocs, mkdocs-macros-plugin, pyproject-api, pytest, tox, molecule (pyproject.toml)
paginate==0.5.7 # via mkdocs-material
pathspec==0.12.1 # via ansible-lint, black, mkdocs, mkdocs-macros-plugin, mypy, yamllint
pexpect==4.9.0 # via molecule (pyproject.toml)
parsley==1.3 # via bindep
pathspec==0.12.1 # via ansible-lint, black, mkdocs, mkdocs-macros-plugin, yamllint
pbr==6.1.1 # via bindep
pexpect==4.9.0 # via ansible-runner, molecule (pyproject.toml)
pillow==11.2.1 # via cairosvg, mkdocs-ansible
platformdirs==4.3.8 # via black, mkdocs-get-deps, pylint, tox, virtualenv
pluggy==1.6.0 # via pytest, tox, molecule (pyproject.toml)
Expand All @@ -93,15 +102,17 @@ pytest-mock==3.14.1 # via molecule (pyproject.toml)
pytest-plus==0.8.1 # via molecule (pyproject.toml)
pytest-testinfra==10.2.2 # via molecule (pyproject.toml)
pytest-xdist==3.7.0 # via molecule (pyproject.toml)
python-daemon==3.1.2 # via ansible-runner
python-dateutil==2.9.0.post0 # via ghp-import, mkdocs-macros-plugin
pyyaml==6.0.2 # via ansible-compat, ansible-core, ansible-lint, mkdocs, mkdocs-get-deps, mkdocs-macros-plugin, pre-commit, pymdown-extensions, pyyaml-env-tag, yamllint, molecule (pyproject.toml)
pyyaml==6.0.2 # via ansible-builder, ansible-compat, ansible-core, ansible-lint, ansible-navigator, ansible-runner, mkdocs, mkdocs-get-deps, mkdocs-macros-plugin, pre-commit, pymdown-extensions, pyyaml-env-tag, yamllint, molecule (pyproject.toml)
pyyaml-env-tag==1.1 # via mkdocs
referencing==0.36.2 # via ansible-lint, jsonschema, jsonschema-specifications, types-jsonschema
requests==2.32.3 # via docker, linkchecker, mkdocs-htmlproofer-plugin, mkdocs-material, molecule (pyproject.toml)
rich==14.0.0 # via enrich, molecule (pyproject.toml)
rpds-py==0.25.1 # via jsonschema, referencing
ruamel-yaml==0.18.12 # via ansible-lint
ruff==0.11.12 # via molecule (pyproject.toml)
setuptools==80.9.0 # via pbr
six==1.17.0 # via python-dateutil
soupsieve==2.7 # via beautifulsoup4
subprocess-tee==0.4.2 # via ansible-compat, ansible-lint
Expand All @@ -115,6 +126,7 @@ tox==4.26.0 # via molecule (pyproject.toml)
types-jsonschema==4.24.0.20250528 # via molecule (pyproject.toml)
types-pexpect==4.9.0.20250516 # via molecule (pyproject.toml)
types-pyyaml==6.0.12.20250516 # via molecule (pyproject.toml)
tzdata==2025.2 # via ansible-navigator
urllib3==2.4.0 # via docker, requests
virtualenv==20.31.2 # via pre-commit, tox
watchdog==6.0.0 # via mkdocs
Expand Down
2 changes: 0 additions & 2 deletions .config/pydoclint-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ tests/integration/test_command.py
DOC103: Function `test_podman`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [app: App].
DOC101: Function `test_docker`: Docstring contains fewer arguments than in function signature.
DOC103: Function `test_docker`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [app: App].
DOC101: Function `test_smoke`: Docstring contains fewer arguments than in function signature.
DOC103: Function `test_smoke`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [app: App].
--------------------
tests/unit/command/init/test_scenario.py
DOC201: Function `fixture_command_args` does not have a return section in docstring
Expand Down
1 change: 1 addition & 0 deletions .config/requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
ansi2html >= 1.8.0
ansible-lint >= 6.12.1
ansible-navigator
coverage[toml]
docker >= 7.1.0 # testing
filelock >= 3.9.0
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,6 @@ tests/fixtures/resources/.extensions/
# docs output
_readthedocs/
.ansible

# Ignore ansible-navigator artifact files in test scenarios
tests/fixtures/integration/**/converge-artifact-*.json
1 change: 1 addition & 0 deletions src/molecule/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ def _get_defaults(self) -> ConfigData:
"role_name_check": 0,
"provisioner": {
"name": "ansible",
"backend": "ansible-playbook",
"config_options": {},
"ansible_args": [],
"connection_options": {},
Expand Down
4 changes: 4 additions & 0 deletions src/molecule/data/molecule.json
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,10 @@
"ProvisionerModel": {
"additionalProperties": true,
"properties": {
"backend": {
"title": "Backend",
"type": "string"
},
"config_options": {
"$ref": "#/$defs/ProvisionerConfigOptionsModel"
},
Expand Down
29 changes: 29 additions & 0 deletions src/molecule/provisioner/ansible.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ class Ansible(base.Base):
log: True
```

Molecule uses ansible-playbook to execute the playbooks. However, it has the
ability to test playbooks inside of an execution-environment using ansible-navigator
backend as a provisioner. This is done by setting ``ansible_navigator`` as the
backend in ``provisioner`` section of ``molecule.yml``.

``` yaml
provisioner:
name: ansible
backend: ansible-navigator
```

The create/destroy playbooks for Docker and Podman are bundled with
Molecule. These playbooks have a clean API from `molecule.yml`, and
are the most commonly used. The bundled playbooks can still be overridden.
Expand Down Expand Up @@ -476,6 +487,24 @@ def name(self) -> str:
"""
return self._config.config["provisioner"]["name"]

@property
def backend(self) -> str:
"""The backend to use for provisioner.

Returns:
The backend to use for provisioner.
"""
return self._config.config["provisioner"]["backend"]

@backend.setter
def backend(self, value: str) -> None:
"""Backend setter.

Args:
value: New backend value of the provisioner.
"""
self._config.config["provisioner"]["backend"] = value

@property
def ansible_args(self) -> list[str]:
"""Provisioner ansible args.
Expand Down
53 changes: 45 additions & 8 deletions src/molecule/provisioner/ansible_playbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import logging
import shlex
import subprocess
import warnings

from typing import TYPE_CHECKING
Expand Down Expand Up @@ -72,7 +73,12 @@ def __init__(
self._env = self._config.provisioner.env

def bake(self) -> None:
"""Bake an ``ansible-playbook`` command so it's ready to execute."""
"""Bake ``ansible-playbook`` or ``navigator run`` command so it's ready to execute.

Raises:
ValueError: when backend is incorrect.
RuntimeError: when ansible-navigator is not available.
"""
if not self._playbook:
return

Expand Down Expand Up @@ -101,13 +107,44 @@ def bake(self) -> None:
else:
ansible_args = []

self._ansible_command = [
"ansible-playbook",
*util.dict2args(options),
*util.bool2args(verbose_flag),
*ansible_args,
self._playbook, # must always go last
]
backend = self._config.provisioner.backend or "ansible-playbook"

if backend:
try:
result = subprocess.run(
[backend, "--version"],
capture_output=True,
text=True,
check=True,
)
LOG.info("%s version: %s", backend, result.stdout.strip())
except subprocess.CalledProcessError as exc:
msg = f"{backend} is not available. Please ensure that it is installed."
raise RuntimeError(msg) from exc

if backend == "ansible-playbook":
self._ansible_command = [
"ansible-playbook",
*util.dict2args(options),
*util.bool2args(verbose_flag),
*ansible_args,
self._playbook, # must always go last
]

elif backend == "ansible-navigator":
self._ansible_command = [
"ansible-navigator",
"run",
self._playbook,
"--mode",
"stdout",
*util.dict2args(options),
*util.bool2args(verbose_flag),
*ansible_args,
]
else:
msg = f"Unsupported backend: {backend}"
raise ValueError(msg)

def execute(self, action_args: list[str] | None = None) -> str: # noqa: ARG002
"""Execute ``ansible-playbook``.
Expand Down
2 changes: 2 additions & 0 deletions src/molecule/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ class ProvisionerData(TypedDict, total=False):

Attributes:
name: Name of the provisioner.
backend: The backend to use for provisioner.
config_options: Configuration options.
ansible_args: Arguments to use with ansible.
connection_options: Options for the connection.
Expand All @@ -157,6 +158,7 @@ class ProvisionerData(TypedDict, total=False):
"""

name: str
backend: str
config_options: dict[str, Any]
ansible_args: list[str]
connection_options: dict[str, Any]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ platforms:
memory: 1Gi
provisioner:
name: ansible
backend: ansible-playbook # or ansible-navigator
config_options:
defaults:
interpreter_python: auto_silent
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
platforms:
- name: localhost
provisioner:
name: ansible
backend: ansible-navigator
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
platforms:
- name: localhost
provisioner:
name: ansible
backend: ansible-playbook
38 changes: 37 additions & 1 deletion tests/integration/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,10 @@ def test_docker(monkeypatch: pytest.MonkeyPatch, test_fixture_dir: Path, app: Ap
assert proc.returncode == 0


def test_smoke(monkeypatch: pytest.MonkeyPatch, test_fixture_dir: Path, app: App) -> None:
def test_smoke(
monkeypatch: pytest.MonkeyPatch,
test_fixture_dir: Path,
) -> None:
"""Execute smoke-test scenario that should spot potentially breaking changes.

Args:
Expand All @@ -499,3 +502,36 @@ def test_smoke(monkeypatch: pytest.MonkeyPatch, test_fixture_dir: Path, app: App
command = ["molecule", "test", "--scenario-name", "smoke"]
result = run(command)
assert result.returncode == 0, result


def test_with_provisioner_backend_as_ansible_playbook(
monkeypatch: pytest.MonkeyPatch,
test_fixture_dir: Path,
) -> None:
"""Execute test-scenario (smoke test) that should spot potentially breaking changes.

Args:
monkeypatch: Pytest fixture.
test_fixture_dir: Path to the test fixture directory.
"""
monkeypatch.chdir(test_fixture_dir)
command = ["molecule", "test", "--scenario-name", "test-scenario"]
result = run(command)
assert result.returncode == 0, result


@mac_on_gh
def test_with_provisioner_backend_as_ansible_navigator(
monkeypatch: pytest.MonkeyPatch,
test_fixture_dir: Path,
) -> None:
"""Execute test-scenario-for-nav (smoke test) that should spot potentially breaking changes.

Args:
monkeypatch: Pytest fixture.
test_fixture_dir: Path to the test fixture directory.
"""
monkeypatch.chdir(test_fixture_dir)
command = ["molecule", "test", "--scenario-name", "test-scenario-for-nav"]
result = run(command)
assert result.returncode == 0, result
22 changes: 22 additions & 0 deletions tests/unit/provisioner/test_ansible_playbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,28 @@ def test_bake(_inventory_directory, _instance): # type: ignore[no-untyped-def]
assert _instance._ansible_command == args


def test_bake_with_ansible_navigator(_inventory_directory, _instance): # type: ignore[no-untyped-def] # noqa: ANN201, PT019, D103
pb = _instance._config.provisioner.playbooks.converge
_instance._playbook = pb
_instance._config.provisioner.backend = "ansible-navigator"
_instance.bake()

args = [
"ansible-navigator",
"run",
pb,
"--mode",
"stdout",
"--become",
"--inventory",
_inventory_directory,
"--skip-tags",
"molecule-notest,notest",
]

assert _instance._ansible_command == args


def test_bake_removes_non_interactive_options_from_non_converge_playbooks( # type: ignore[no-untyped-def] # noqa: ANN201, D103
_inventory_directory, # noqa: PT019
_instance, # noqa: PT019
Expand Down
Loading