Repository: ansible/molecule

Issue:
base-config not represented in ansible provisioner `molecule_yml`
# Issue Type

- Bug report

# Molecule and Ansible details

```
$ ansible --version                                                                                                                      
ansible 2.6.2
```

```
$ molecule --version
molecule, version 2.16.0
```

Molecule installation method (one of):

- pip

Ansible installation method (one of):

- pip

# Desired Behavior

I have a monolith repo with many roles (50+) so there is value to me to have a shared `molecule.yml`.

The directory structure looks like
```
molecule/
  resources/
    molecule.yml <-- base config
roles/
  A/
    molecule/
      default/
        molecule.yml
  B/
    molecule/
      default/
        molecule.yml <-- role config
```

The `base-config` consists of
```
---
dependency:
  name: galaxy
driver:
  name: lxd
platforms:
  - name: instance-xenial
    ...
```
and the `role-config` files are empty.

I expect that running `molecule --base-config ../../molecule/resources/molecule.yml destroy` would merge the two files and preserve the `platforms` configuration.

# Actual Behaviour (Bug report only)

`platforms` is not preserved, and running `molecule --base-config ../../molecule/resources/molecule.yml destroy` yields:
```
$ MOLECULE_DEBUG=True molecule -c ../../molecule/resources/molecule.yml destroy
--> Validating schema /home/me/src/molecule/ansible-playbooks/roles/docker/molecule/default/molecule.yml.
Validation completed successfully.

--> Test matrix
    
└── default
    └── destroy
    
--> Scenario: 'default'
--> Action: 'destroy'
    
    PLAY [Destroy] *****************************************************************
    
    TASK [Destroy molecule instance(s)] ********************************************
    fatal: [localhost]: FAILED! => {"msg": "'dict object' has no attribute 'platforms'"}
    
    PLAY RECAP *********************************************************************
    localhost                  : ok=0    changed=0    unreachable=0    failed=1   
    
    
ERROR:
```

Maybe this is a dup of #328 but I do not expect an empty `role-config` file to overwrite the `base-config`.


Discussion:
#1328 You cannot override platforms.
@retr0h it seems to me that my use case is different because I'm not trying to merge `platforms` lists.  My `molecule/default/molecule.yml` is completely empty.

I am not trying to override platforms, but inherit them from the base config.
I think my problem is that `molecule_yml` as passed into the ansible provisioner is apparently not the merged config, but instead the empty file.
I'm not sure how or if it will work.  The molecule_yml gets evaluated by ansible later on.  Can you push this to a repo where I can debug futher?
I've added a repo at https://github.com/uSpike/molecule_debug

There's a README to explain what to do to reproduce it, but here's my experience:

```
roles/example$ molecule --debug -c ../../molecule/resources/molecule.yml test

<snip>

    ansible-playbook 2.6.2
      config file = /tmp/molecule/example/default/ansible.cfg
      configured module search path = ['/home/me/src/mexample/venv/lib/python3.6/site-packages/molecule/provisioner/ansible/plugins/libraries', '/tmp/molecule/example/default/library', '/home/me/src/mexample/roles/example/library']
      ansible python module location = /home/me/src/mexample/venv/lib/python3.6/site-packages/ansible
      executable location = /home/me/src/mexample/venv/bin/ansible-playbook
      python version = 3.6.4 (default, Jul 17 2018, 22:35:18) [GCC 5.4.0 20160609]
    Using /tmp/molecule/example/default/ansible.cfg as config file
    Parsed /tmp/molecule/example/default/ansible_inventory.yml inventory source with yaml plugin
    
    PLAYBOOK: destroy.yml **********************************************************
    1 plays in /home/me/src/mexample/venv/lib/python3.6/site-packages/molecule/provisioner/ansible/playbooks/docker/destroy.yml
    
    PLAY [Destroy] *****************************************************************
    META: ran handlers
    
    TASK [Destroy molecule instance(s)] ********************************************
    task path: /home/me/src/mexample/venv/lib/python3.6/site-packages/molecule/provisioner/ansible/playbooks/docker/destroy.yml:8
    fatal: [localhost]: FAILED! => {
        "msg": "'dict object' has no attribute 'platforms'"
    }
    
    PLAY RECAP *********************************************************************
    localhost                  : ok=0    changed=0    unreachable=0    failed=1   
    
    
ERROR: 
An error occurred during the test sequence action: 'destroy'. Cleaning up.
```

The failure is here: https://github.com/metacloud/molecule/blob/master/molecule/provisioner/ansible/playbooks/docker/destroy.yml#L15 since `molecule_yml` does not contain the `base-config` configuration.

A naive solution to this issue is here: https://github.com/uSpike/molecule/commit/96d07af89cfd23bdb76622ce79b1a1fd1bee6642
thx @uSpike 
> A naive solution to this issue is here: uSpike@96d07af

Not a bad solution at all.

Actually, maybe it is.  That will end up having Molecule persist the contents of the config into $TMPDIR/molecule/, which could expose secrets safely fetched in 'molecule.yml' by lookups or env vars.
An idea:
* Expose `MOLECULE_FILE_BASE` variable
* Modify `molecule_from_yaml` filter to accept a list of files

This might end up looking like
```
# Molecule managed

---
all:
  vars:
    molecule_ephemeral_directory: '{{ lookup(''env'', ''MOLECULE_EPHEMERAL_DIRECTORY'')
      }}'
    molecule_file: '{{ lookup(''env'', ''MOLECULE_FILE'') }}'
    molecule_file_base: '{{ lookup(''env'', ''MOLECULE_FILE_BASE'') }}'
    molecule_instance_config: '{{ lookup(''env'', ''MOLECULE_INSTANCE_CONFIG'') }}'
    molecule_scenario_directory: '{{ lookup(''env'', ''MOLECULE_SCENARIO_DIRECTORY'')
      }}'
    molecule_yml: '{{ [lookup(''file'', molecule_file_base), lookup(''file'', molecule_file)]  | molecule_from_yaml }}'
```

I opened #1437 with an implementation, would love some feedback.
@retr0h any thoughts on this?  It is rather painful to manage many scenarios without being able to use the base config.
I'm going to hold off from merging this.  As you know [Ansible is adopting Molecule](https://groups.google.com/forum/#!topic/ansible-project/ehrb6AEptzA), therefore I do not want to merge anything that may impact future architectural decisions, as I will no longer be leading the development of this project.
@retr0h @uSpike any update on this? I am running into this issue currently while trying to find a middle ground between developing/testing my roles locally and testing in Travis CI. Here's my use case:

When developing/testing locally, I want to `converge` against all platforms with a single command, but when using Travis CI, I want to run each platform individually so it can be properly parallelized by the Travis `env` matrix.

So for local development, I want something like this:

```yaml
# molecule/default/molecule.yml
...
platforms:
  - name: ubuntu1804
    image: "geerlingguy/docker-ubuntu1804-ansible:latest"
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    privileged: true
    pre_build_image: true
  - name: centos7
    image: "geerlingguy/docker-centos7-ansible:latest"
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    privileged: true
    pre_build_image: true
...
```

And then for Travis CI testing:

```yaml
# molecule/default/molecule.yml
...
platforms:
  - name: instance
    image: "geerlingguy/docker-${PLATFORM_DISTRO:-centos7}-ansible:latest"
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    privileged: true
    pre_build_image: true
...
```

```yaml
# .travis.yml
...
env:
  matrix:
    - PLATFORM_DISTRO: centos7
    - PLATFORM_DISTRO: ubuntu1804
...
```

Looking at the limited documentation for `--base-config` it seemed to be the right approach. I created two files in the `molecule` directory: `molecule/base_config_default.yml` and `molecule/base_config_travis.yml`:

```yaml
# molecule/base_config_default.yml
---
platforms:
  - name: ubuntu1804
    image: "geerlingguy/docker-ubuntu1804-ansible:latest"
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    privileged: true
    pre_build_image: true
  - name: centos7
    image: "geerlingguy/docker-centos7-ansible:latest"
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    privileged: true
    pre_build_image: true
```

```yaml
# molecule/base_config_travis.yml
---
platforms:
  - name: instance
    image: "geerlingguy/docker-${PLATFORM_DISTRO:-centos7}-ansible:latest"
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    privileged: true
    pre_build_image: true
```

Then for local development, I could use `--base-config molecule/base_config_default.yml` and for Travis, I can use `--base-config molecule/base_config_travis.yml` when running `molecule` commands.

While developing the PoC for this approach, I ran into the same problem as @uSpike because it seems that the `platforms` key is not available to the `create/destroy` playbooks:

```shell
    TASK [Destroy molecule instance(s)] ********************************************
    task path: /Users/percy/.asdf/installs/python/2.7.15/lib/python2.7/site-packages/molecule/provisioner/ansible/playbooks/docker/destroy.yml:8
    fatal: [localhost]: FAILED! => {
        "msg": "'dict object' has no attribute 'platforms'"
    }

    PLAY RECAP *********************************************************************
    localhost                  : ok=0    changed=0    unreachable=0    failed=1
```

Another approach I will try is to create different a different `scenario` for Travis, although I suspect this way will be less DRY. Let me know if anyone has any other suggestions.

Thanks!

Hmmm, https://github.com/ansible/molecule/issues/1661 seems related /cc @Rickkwa, in the way that there are missing keys available when using this `--base-config`. @percygrunwald, thanks for your nice report. I don't think anyone is working to fix this issue right now unfortunately. If you have time for a fix, that would be great.
Documentation of base conf and env would be nice. 
@uSpike Do you still need this one? Can you rebase or close it?
@ssbarnea I'll rebase and reopen if it is a welcomed merge!
I've updated #1437 for those that are interested.

Resolving patch:
diff --git a/molecule/command/base.py b/molecule/command/base.py
--- a/molecule/command/base.py
+++ b/molecule/command/base.py
@@ -68,6 +68,7 @@ def _setup(self):
 
         :return: None
         """
+        self._config.write()
         self._config.provisioner.write_config()
         self._config.provisioner.manage_inventory()
 
diff --git a/molecule/config.py b/molecule/config.py
--- a/molecule/config.py
+++ b/molecule/config.py
@@ -103,6 +103,13 @@ def after_init(self):
         if self.molecule_file:
             self._validate()
 
+    def write(self):
+        util.write_file(self.config_file, util.safe_dump(self.config))
+
+    @property
+    def config_file(self):
+        return os.path.join(self.scenario.ephemeral_directory, MOLECULE_FILE)
+
     @property
     def is_parallel(self):
         return self.command_args.get('parallel', False)
@@ -169,7 +176,7 @@ def drivers(self):
     def env(self):
         return {
             'MOLECULE_DEBUG': str(self.debug),
-            'MOLECULE_FILE': self.molecule_file,
+            'MOLECULE_FILE': self.config_file,
             'MOLECULE_ENV_FILE': self.env_file,
             'MOLECULE_STATE_FILE': self.state.state_file,
             'MOLECULE_INVENTORY_FILE': self.provisioner.inventory_file,


Test patch:
diff --git a/molecule/test/unit/command/test_base.py b/molecule/test/unit/command/test_base.py
--- a/molecule/test/unit/command/test_base.py
+++ b/molecule/test/unit/command/test_base.py
@@ -111,8 +111,8 @@ def test_setup(
     _patched_manage_inventory,
     _instance,
 ):
-
     assert os.path.isdir(os.path.dirname(_instance._config.provisioner.inventory_file))
+    assert os.path.isfile(_instance._config.config_file)
 
     _patched_manage_inventory.assert_called_once_with()
     _patched_write_config.assert_called_once_with()
diff --git a/molecule/test/unit/test_config.py b/molecule/test/unit/test_config.py
--- a/molecule/test/unit/test_config.py
+++ b/molecule/test/unit/test_config.py
@@ -183,7 +183,7 @@ def test_env(config_instance):
     config_instance.args = {'env_file': '.env'}
     x = {
         'MOLECULE_DEBUG': 'False',
-        'MOLECULE_FILE': config_instance.molecule_file,
+        'MOLECULE_FILE': config_instance.config_file,
         'MOLECULE_ENV_FILE': util.abs_path(config_instance.args.get('env_file')),
         'MOLECULE_INVENTORY_FILE': config_instance.provisioner.inventory_file,
         'MOLECULE_EPHEMERAL_DIRECTORY': config_instance.scenario.ephemeral_directory,
@@ -494,3 +494,9 @@ def test_set_env_from_file_returns_original_env_when_env_file_not_found(
     env = config.set_env_from_file({}, 'file-not-found')
 
     assert {} == env
+
+
+def test_write_config(config_instance):
+    config_instance.write()
+
+    assert os.path.isfile(config_instance.config_file)