Skip to content

Commit

Permalink
Added string interpolation to molecule.yml (#709)
Browse files Browse the repository at this point in the history
Allows the embedding of environment variables inside molecule.yml.
Originally requested in #643.

Fixes: #707
  • Loading branch information
retr0h committed Jan 12, 2017
1 parent ed0f5ca commit c4fa4f1
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 6 deletions.
9 changes: 7 additions & 2 deletions doc/source/configuration.rst
Expand Up @@ -5,6 +5,11 @@ TODO(retr0h): Talk about config loading...
How it finds files
and scenarios

Variable Substitution
---------------------

.. autoclass:: molecule.interpolation.Interpolator
:undoc-members:

Dependency
----------
Expand Down Expand Up @@ -43,7 +48,7 @@ any provider `Ansible`_ supports. This work is offloaded to the `provisioner`.
Docker
^^^^^^

.. autoclass:: molecule.driver.docker.Docker
.. autoclass:: molecule.driver.dockr.Dockr
:undoc-members:

Lint
Expand Down Expand Up @@ -85,7 +90,7 @@ Molecule handles provisioning and converging the role.
Ansible
^^^^^^^

.. autoclass:: molecule.provisioner.Ansible
.. autoclass:: molecule.provisioner.ansible.Ansible
:undoc-members:

Scenario
Expand Down
9 changes: 7 additions & 2 deletions molecule/command/base.py
Expand Up @@ -25,6 +25,7 @@
import yaml

from molecule import config
from molecule import interpolation
from molecule import util


Expand All @@ -50,13 +51,17 @@ def execute(self): # pragma: no cover

def _load_config(config):
"""
Open and YAML parse the provided file and returns a dict.
Open, interpolate, and YAML parse the provided file and returns a dict.
:param config: A string containing an absolute path to a Molecule config.
:return: dict
"""
i = interpolation.Interpolator(interpolation.TemplateWithDefaults,
os.environ)
with open(config, 'r') as stream:
return yaml.safe_load(stream) or {}
interpolated_config = i.interpolate(stream.read())

return yaml.safe_load(interpolated_config) or {}


def _verify_configs(configs):
Expand Down
89 changes: 89 additions & 0 deletions molecule/interpolation.py
@@ -0,0 +1,89 @@
# Copyright 2015 Docker, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

# Taken from Docker Compose:
# https://github.com/docker/compose/blob/master/compose/config/interpolation.py

import string


class InvalidInterpolation(Exception):
def __init__(self, string):
self.string = string


class Interpolator(object):
"""
Configuration options may contain environment variables. For example,
suppose the shell contains `VERIFIER_NAME=testinfra` and the following
molecule.yml is supplied.
.. code-block:: yaml
verifier:
- name: ${VERIFIER_NAME}
Molecule will substitute `$VERIFIER_NAME` with the value of the
`VERIFIER_NAME` environment variable.
.. warning::
If an environment variable is not set, Molecule substitutes with an empty
string.
Both `$VARIABLE` and `${VARIABLE}` syntax are supported. Extended
shell-style features, such as `${VARIABLE-default}` and
`${VARIABLE:-default}` are also supported.
If a literal dollar sign is needed in a configuration, use a double dollar
sign (`$$`).
"""

def __init__(self, templater, mapping):
self.templater = templater
self.mapping = mapping

def interpolate(self, string):
try:
return self.templater(string).substitute(self.mapping)
except ValueError:
raise InvalidInterpolation(string)


class TemplateWithDefaults(string.Template):
idpattern = r'[_a-z][_a-z0-9]*(?::?-[^}]+)?'

# Modified from python2.7/string.py
def substitute(self, mapping):
# Helper function for .sub()
def convert(mo):
# Check the most common path first.
named = mo.group('named') or mo.group('braced')
if named is not None:
if ':-' in named:
var, _, default = named.partition(':-')
return mapping.get(var) or default
if '-' in named:
var, _, default = named.partition('-')
return mapping.get(var, default)
val = mapping.get(named, '')
return '%s' % (val, )
if mo.group('escaped') is not None:
return self.delimiter
if mo.group('invalid') is not None:
self._invalid(mo)
raise ValueError('Unrecognized named group in pattern',
self.pattern)

return self.pattern.sub(convert, self.template)
2 changes: 1 addition & 1 deletion molecule/provisioner/ansible.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015-2017 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
Expand Down
2 changes: 1 addition & 1 deletion test/unit/conftest.py
Expand Up @@ -21,8 +21,8 @@
import os

import pytest
import yaml

from molecule import util
from molecule import config


Expand Down
120 changes: 120 additions & 0 deletions test/unit/test_interpolation.py
@@ -0,0 +1,120 @@
# Copyright 2015 Docker, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import pytest

from molecule import interpolation


@pytest.fixture
def mock_env():
return {
'FOO': 'foo',
'BAR': '',
'DEPENDENCY_NAME': 'galaxy',
'VERIFIER_NAME': 'testinfra'
}


@pytest.fixture
def interpolator_instance(mock_env):
return interpolation.Interpolator(interpolation.TemplateWithDefaults,
mock_env).interpolate


def test_escaped_interpolation(interpolator_instance):
assert '${foo}' == interpolator_instance('$${foo}')


def test_invalid_interpolation(interpolator_instance):
with pytest.raises(interpolation.InvalidInterpolation):
interpolator_instance('${')
with pytest.raises(interpolation.InvalidInterpolation):
interpolator_instance('$}')
with pytest.raises(interpolation.InvalidInterpolation):
interpolator_instance('${}')
with pytest.raises(interpolation.InvalidInterpolation):
interpolator_instance('${ }')
with pytest.raises(interpolation.InvalidInterpolation):
interpolator_instance('${ foo}')
with pytest.raises(interpolation.InvalidInterpolation):
interpolator_instance('${foo }')
with pytest.raises(interpolation.InvalidInterpolation):
interpolator_instance('${foo!}')


def test_interpolate_missing_no_default(interpolator_instance):
assert 'This var' == interpolator_instance('This ${missing} var')
assert 'This var' == interpolator_instance('This ${BAR} var')


def test_interpolate_with_value(interpolator_instance):
assert 'This foo var' == interpolator_instance('This $FOO var')
assert 'This foo var' == interpolator_instance('This ${FOO} var')


def test_interpolate_missing_with_default(interpolator_instance):
assert 'ok def' == interpolator_instance('ok ${missing:-def}')
assert 'ok def' == interpolator_instance('ok ${missing-def}')
assert 'ok /non:-alphanumeric' == interpolator_instance(
'ok ${BAR:-/non:-alphanumeric}')


def test_interpolate_with_empty_and_default_value(interpolator_instance):
assert 'ok def' == interpolator_instance('ok ${BAR:-def}')
assert 'ok ' == interpolator_instance('ok ${BAR-def}')


def test_interpolate_with_molecule_yaml(interpolator_instance):
data = """
---
dependency:
name: $DEPENDENCY_NAME
driver:
name: docker
lint:
name: ansible-lint
platforms:
- name: instance-1
provisioner:
name: ansible
scenario:
name: default
verifier:
name: ${VERIFIER_NAME}
options:
$FOO: bar
""".strip()

x = """
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: ansible-lint
platforms:
- name: instance-1
provisioner:
name: ansible
scenario:
name: default
verifier:
name: testinfra
options:
foo: bar
""".strip()

assert x == interpolator_instance(data)

0 comments on commit c4fa4f1

Please sign in to comment.