Repository: ansible/molecule

Issue:
Ansible Verifier does not pass its env variables to the verify step
<!--- Verify first that your issue is not already reported on GitHub -->
<!--- Do not report bugs before reproducing them with the code of the main branch! -->
<!--- Please also check https://molecule.readthedocs.io/en/latest/faq.html --->
<!--- Please use https://groups.google.com/forum/#!forum/molecule-users for usage questions -->

# Issue Type

- Bug report

# Molecule and Ansible details

```
ansible [core 2.12.3]
  config file = None
  configured module search path = ['/Users/g/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /Users/g/.pyenv/versions/3.10.3/envs/ansible-avd/lib/python3.10/site-packages/ansible
  ansible collection location = /Users/g/.ansible/collections:/usr/share/ansible/collections
  executable location = /Users/g/.pyenv/versions/ansible-avd/bin/ansible
  python version = 3.10.3 (main, Mar 18 2022, 09:31:01) [Clang 13.0.0 (clang-1300.0.29.30)]
  jinja version = 3.0.3
  libyaml = False
molecule 3.4.1 using python 3.10
    ansible:2.12.3
    delegated:3.4.1 from molecule
    docker:1.1.0 from molecule_docker requiring collections: community.docker>=1.9.1

```

Molecule installation method (one of):

- pip

Ansible installation method (one of):

- pip

Detail any linters or test runners used:

# Desired Behavior

env variables given to the verifier are usable in the verify playbook. i.e. in the output below the debug message should read `VERIFIER`

# Actual Behaviour

Please give some details of what is actually happening.
Include a [minimum complete verifiable example](https://stackoverflow.com/help/mcve) with
output of running `molecule --debug`.

The current Ansible Verifier is calling the Provisionner `verify` function: https://github.com/amarao/molecule/blob/master/src/molecule/verifier/ansible.py#L83. This means that env variables given to the verifier section are not used but rather the ones from the provisionner are:


```
g% cat molecule/default/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: delegated
platforms:
  - name: instance
provisioner:
  name: ansible
  env:
    DEMO: "PROVISIONER"
verifier:
  name: ansible
  env:
    DEMO: "VERIFIER"
g% cat molecule/default/verify.yml
---
# This is an example playbook to execute Ansible tests.

- name: Verify
  hosts: all
  gather_facts: false
  tasks:
  - name: Example assertion
    debug:
      msg: "{{ lookup('env', 'DEMO') }}"

```

will result in:

```
g% molecule verify
[...]
PLAY [Verify] ******************************************************************

TASK [Example assertion] *******************************************************
ok: [instance] => {
    "msg": "PROVISIONER"
}
``` 



Discussion:

Resolving patch:
diff --git a/src/molecule/provisioner/ansible.py b/src/molecule/provisioner/ansible.py
--- a/src/molecule/provisioner/ansible.py
+++ b/src/molecule/provisioner/ansible.py
@@ -770,7 +770,8 @@ def verify(self, action_args=None):
             LOG.warning("Skipping, verify playbook not configured.")
             return
         for playbook in playbooks:
-            pb = self._get_ansible_playbook(playbook)
+            # Get ansible playbooks for `verify` instead of `provision`
+            pb = self._get_ansible_playbook(playbook, verify=True)
             pb.execute()
 
     def write_config(self):
@@ -872,16 +873,20 @@ def _link_or_update_vars(self):
             LOG.info(msg)
             os.symlink(source, target)
 
-    def _get_ansible_playbook(self, playbook, **kwargs):
+    def _get_ansible_playbook(self, playbook, verify=False, **kwargs):
         """
         Get an instance of AnsiblePlaybook and returns it.
 
         :param playbook: A string containing an absolute path to a
          provisioner's playbook.
+        :param verify: An optional bool to toggle the Plabook mode between
+         provision and verify. False: provision; True: verify. Default is False.
         :param kwargs: An optional keyword arguments.
         :return: object
         """
-        return ansible_playbook.AnsiblePlaybook(playbook, self._config, **kwargs)
+        return ansible_playbook.AnsiblePlaybook(
+            playbook, self._config, verify, **kwargs
+        )
 
     def _verify_inventory(self):
         """
diff --git a/src/molecule/provisioner/ansible_playbook.py b/src/molecule/provisioner/ansible_playbook.py
--- a/src/molecule/provisioner/ansible_playbook.py
+++ b/src/molecule/provisioner/ansible_playbook.py
@@ -31,22 +31,29 @@
 class AnsiblePlaybook(object):
     """Privisioner Playbook."""
 
-    def __init__(self, playbook, config):
+    def __init__(self, playbook, config, verify=False):
         """
         Set up the requirements to execute ``ansible-playbook`` and returns \
         None.
 
         :param playbook: A string containing the path to the playbook.
         :param config: An instance of a Molecule config.
+        :param verify: An optional bool to toggle the Plabook mode between
+         provision and verify. False: provision; True: verify. Default is False.
         :returns: None
         """
         self._ansible_command = None
         self._playbook = playbook
         self._config = config
         self._cli = {}
-        self._env = util.merge_dicts(
-            self._config.provisioner.env, self._config.runtime.environ
-        )
+        if verify:
+            self._env = util.merge_dicts(
+                self._config.verifier.env, self._config.config["verifier"]["env"]
+            )
+        else:
+            self._env = util.merge_dicts(
+                self._config.provisioner.env, self._config.runtime.environ
+            )
 
     def bake(self):
         """


Test patch:
diff --git a/src/molecule/test/unit/provisioner/test_ansible.py b/src/molecule/test/unit/provisioner/test_ansible.py
--- a/src/molecule/test/unit/provisioner/test_ansible.py
+++ b/src/molecule/test/unit/provisioner/test_ansible.py
@@ -331,7 +331,7 @@ def test_check(_instance, mocker, _patched_ansible_playbook):
     _instance.check()
 
     _patched_ansible_playbook.assert_called_once_with(
-        _instance._config.provisioner.playbooks.converge, _instance._config
+        _instance._config.provisioner.playbooks.converge, _instance._config, False
     )
     _patched_ansible_playbook.return_value.add_cli_arg.assert_called_once_with(
         "check", True
@@ -343,7 +343,7 @@ def test_converge(_instance, mocker, _patched_ansible_playbook):
     result = _instance.converge()
 
     _patched_ansible_playbook.assert_called_once_with(
-        _instance._config.provisioner.playbooks.converge, _instance._config
+        _instance._config.provisioner.playbooks.converge, _instance._config, False
     )
     # NOTE(retr0h): This is not the true return type.  This is a mock return
     #               which didn't go through str.decode().
@@ -355,7 +355,9 @@ def test_converge(_instance, mocker, _patched_ansible_playbook):
 def test_converge_with_playbook(_instance, mocker, _patched_ansible_playbook):
     result = _instance.converge("playbook")
 
-    _patched_ansible_playbook.assert_called_once_with("playbook", _instance._config)
+    _patched_ansible_playbook.assert_called_once_with(
+        "playbook", _instance._config, False
+    )
     # NOTE(retr0h): This is not the true return type.  This is a mock return
     #               which didn't go through str.decode().
     assert result == b"patched-ansible-playbook-stdout"
@@ -367,7 +369,7 @@ def test_cleanup(_instance, mocker, _patched_ansible_playbook):
     _instance.cleanup()
 
     _patched_ansible_playbook.assert_called_once_with(
-        _instance._config.provisioner.playbooks.cleanup, _instance._config
+        _instance._config.provisioner.playbooks.cleanup, _instance._config, False
     )
     _patched_ansible_playbook.return_value.execute.assert_called_once_with()
 
@@ -376,7 +378,7 @@ def test_destroy(_instance, mocker, _patched_ansible_playbook):
     _instance.destroy()
 
     _patched_ansible_playbook.assert_called_once_with(
-        _instance._config.provisioner.playbooks.destroy, _instance._config
+        _instance._config.provisioner.playbooks.destroy, _instance._config, False
     )
     _patched_ansible_playbook.return_value.execute.assert_called_once_with()
 
@@ -385,7 +387,7 @@ def test_side_effect(_instance, mocker, _patched_ansible_playbook):
     _instance.side_effect()
 
     _patched_ansible_playbook.assert_called_once_with(
-        _instance._config.provisioner.playbooks.side_effect, _instance._config
+        _instance._config.provisioner.playbooks.side_effect, _instance._config, False
     )
     _patched_ansible_playbook.return_value.execute.assert_called_once_with()
 
@@ -394,7 +396,7 @@ def test_create(_instance, mocker, _patched_ansible_playbook):
     _instance.create()
 
     _patched_ansible_playbook.assert_called_once_with(
-        _instance._config.provisioner.playbooks.create, _instance._config
+        _instance._config.provisioner.playbooks.create, _instance._config, False
     )
     _patched_ansible_playbook.return_value.execute.assert_called_once_with()
 
@@ -403,7 +405,7 @@ def test_prepare(_instance, mocker, _patched_ansible_playbook):
     _instance.prepare()
 
     _patched_ansible_playbook.assert_called_once_with(
-        _instance._config.provisioner.playbooks.prepare, _instance._config
+        _instance._config.provisioner.playbooks.prepare, _instance._config, False
     )
     _patched_ansible_playbook.return_value.execute.assert_called_once_with()
 
@@ -412,7 +414,7 @@ def test_syntax(_instance, mocker, _patched_ansible_playbook):
     _instance.syntax()
 
     _patched_ansible_playbook.assert_called_once_with(
-        _instance._config.provisioner.playbooks.converge, _instance._config
+        _instance._config.provisioner.playbooks.converge, _instance._config, False
     )
     _patched_ansible_playbook.return_value.add_cli_arg.assert_called_once_with(
         "syntax-check", True
diff --git a/src/molecule/test/unit/provisioner/test_ansible_playbook.py b/src/molecule/test/unit/provisioner/test_ansible_playbook.py
--- a/src/molecule/test/unit/provisioner/test_ansible_playbook.py
+++ b/src/molecule/test/unit/provisioner/test_ansible_playbook.py
@@ -33,6 +33,49 @@ def _instance(config_instance):
     return _instance
 
 
+@pytest.fixture
+def _provisioner_section_data():
+    return {"provisioner": {"name": "ansible", "env": {"FOO": "bar"}}}
+
+
+@pytest.fixture
+def _verifier_section_data():
+    return {"verifier": {"name": "ansible", "env": {"FOO": "bar"}}}
+
+
+@pytest.fixture
+def _provisioner_verifier_section_data():
+    return {
+        "provisioner": {"name": "ansible", "env": {"FOO": "bar"}},
+        "verifier": {"name": "ansible", "env": {"FOO": "baz"}},
+    }
+
+
+@pytest.fixture
+def _instance_for_verifier_env(config_instance):
+    _instance = ansible_playbook.AnsiblePlaybook("playbook", config_instance, True)
+    return _instance
+
+
+@pytest.mark.parametrize(
+    "config_instance", ["_provisioner_section_data"], indirect=True
+)
+def test_env_in_provision(_instance_for_verifier_env):
+    assert "bar" == _instance_for_verifier_env._env["FOO"]
+
+
+@pytest.mark.parametrize("config_instance", ["_verifier_section_data"], indirect=True)
+def test_env_in_verifier(_instance_for_verifier_env):
+    assert "bar" == _instance_for_verifier_env._env["FOO"]
+
+
+@pytest.mark.parametrize(
+    "config_instance", ["_provisioner_verifier_section_data"], indirect=True
+)
+def test_env_in_verify_override_provision(_instance_for_verifier_env):
+    assert "baz" == _instance_for_verifier_env._env["FOO"]
+
+
 @pytest.fixture
 def _inventory_directory(_instance):
     return _instance._config.provisioner.inventory_directory