Skip to content

Commit

Permalink
lifted molecule code to add env var support (#39)
Browse files Browse the repository at this point in the history
* lifted molecule code to add env var support

* made include consistent & improved doc string

* fix test name
  • Loading branch information
ephur authored and retr0h committed May 23, 2017
1 parent da8c2ea commit 25649f7
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 1 deletion.
8 changes: 7 additions & 1 deletion gilt/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import giturlparse
import yaml

from gilt import interpolation


class ParseError(Exception):
""" Error raised when a config can't be loaded properly. """
Expand Down Expand Up @@ -128,9 +130,13 @@ def _get_config(filename):
:parse filename: A string containing the path to YAML file.
:return: dict
"""
i = interpolation.Interpolator(interpolation.TemplateWithDefaults,
os.environ)

with open(filename, 'r') as stream:
try:
return yaml.safe_load(stream)
interpolated_config = i.interpolate(stream.read())
return yaml.safe_load(interpolated_config)
except yaml.parser.ParserError as e:
msg = 'Error parsing gilt config: {0}'.format(e)
raise ParseError(msg)
Expand Down
81 changes: 81 additions & 0 deletions gilt/interpolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 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:https://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 `ETCD_VERSION=1.0` and the following
gilt.yml is supplied.
.. code-block:: yaml
- git: https://github.com/retr0h/ansible-etcd.git
version: ${ETCD_VERSION}
dst: roles/retr0h.ansible-etcd-${ETCD_VERSION}/
will substitute `${ETCD_VERSION}` with the value of the
`ETCD_VERSION` environment variable.
.. warning::
If an environment variable is not set, gilt 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)

return self.pattern.sub(convert, self.template)
89 changes: 89 additions & 0 deletions test/test_interpolation.py
Original file line number Diff line number Diff line change
@@ -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:https://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 gilt import interpolation


@pytest.fixture
def mock_env():
return {'FOO': 'foo', 'BAR': '', 'ETCD_VERSION': 'master'}


@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_gilt_yaml(interpolator_instance):
data = """
---
git: [email protected]:retr0h/ansible-etcd.git
version: ${ETCD_VERSION}
dst: roles/etcd-$ETCD_VERSION
""".strip()

x = """
---
git: [email protected]:retr0h/ansible-etcd.git
version: master
dst: roles/etcd-master
""".strip()

assert x == interpolator_instance(data)

0 comments on commit 25649f7

Please sign in to comment.