Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Yaml #14

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open

Yaml #14

Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 43 additions & 13 deletions Rake/Gemfile/molecule/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import os

import anyconfig
import six

from molecule import interpolation
from molecule import logger
Expand Down Expand Up @@ -53,6 +54,15 @@
MERGE_STRATEGY = anyconfig.MS_DICTS


# https://stackoverflow.com/questions/16017397/injecting-function-call-after-init-with-decorator # noqa
class NewInitCaller(type):
def __call__(cls, *args, **kwargs):
obj = type.__call__(cls, *args, **kwargs)
obj.after_init()
return obj


@six.add_metaclass(NewInitCaller)
class Config(object):
"""
Molecule searches the current directory for `molecule.yml` files by
Expand Down Expand Up @@ -90,9 +100,11 @@ def __init__(self,
self.args = args
self.command_args = command_args
self.ansible_args = ansible_args
self.config = self._combine()
self.config = self._get_config()
self._action = None

def after_init(self):
self.config = self._reget_config()
self._validate()

@property
Expand Down Expand Up @@ -245,7 +257,29 @@ def _get_driver_name(self):

return driver_name

def _combine(self):
def _get_config(self):
"""
Perform a prioritized recursive merge of config files, and returns
a new dict. Prior to merging the config files are interpolated with
environment variables.

:return: dict
"""
return self._combine(keep_string='MOLECULE_')

def _reget_config(self):
"""
Perform the same prioritized recursive merge from `get_config`, this
time, interpolating the `keep_string` left behind in the original
`get_config` call. This is probably __very__ bad.

:return: dict
"""
env = util.merge_dicts(os.environ.copy(), self.env)

return self._combine(env=env)

def _combine(self, env=os.environ, keep_string=None):
"""
Perform a prioritized recursive merge of config files, and returns
a new dict. Prior to merging the config files are interpolated with
Expand All @@ -263,23 +297,24 @@ def _combine(self):
if base_config:
if os.path.exists(base_config):
with util.open_file(base_config) as stream:
interpolated_config = self._interpolate(stream.read())
interpolated_config = self._interpolate(
stream.read(), env, keep_string)
defaults = util.merge_dicts(
defaults, util.safe_load(interpolated_config))

with util.open_file(self.molecule_file) as stream:
interpolated_config = self._interpolate(stream.read())
interpolated_config = self._interpolate(stream.read(), env,
keep_string)
defaults = util.merge_dicts(defaults,
util.safe_load(interpolated_config))

return defaults

def _interpolate(self, stream):
i = interpolation.Interpolator(interpolation.TemplateWithDefaults,
os.environ)
def _interpolate(self, stream, env, keep_string):
i = interpolation.Interpolator(interpolation.TemplateWithDefaults, env)

try:
return i.interpolate(stream)
return i.interpolate(stream, keep_string)
except interpolation.InvalidInterpolation as e:
msg = ("parsing config file '{}'.\n\n"
'{}\n{}'.format(self.molecule_file, e.place, e.string))
Expand Down Expand Up @@ -398,11 +433,6 @@ def _validate(self):
msg = 'Validating schema {}.'.format(self.molecule_file)
LOG.info(msg)

# Prior to validation, we must set values. This allows us to perform
# validation in one place. This feels gross.
self.config['dependency']['command'] = self.dependency.command
self.config['driver']['name'] = self.driver.name

errors = schema_v2.validate(self.config)
if errors:
msg = "Failed to validate.\n\n{}".format(errors)
Expand Down
6 changes: 5 additions & 1 deletion Rake/Gemfile/molecule/dependency/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ def __init__(self, config):
super(Shell, self).__init__(config)
self._sh_command = None

self.command = config.config['dependency']['command']
# self.command = config..config['dependency']['command']

@property
def command(self):
return self._config.config['dependency']['command']

@property
def default_options(self):
Expand Down
12 changes: 9 additions & 3 deletions Rake/Gemfile/molecule/interpolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,19 @@ class Interpolator(object):

If a literal dollar sign is needed in a configuration, use a double dollar
sign (`$$`).

Molecule will substitute special `MOLECULE_` environment variables defined
in `molecule.yml`. However, use at your own risk, this should be used
sparingly.
"""

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

def interpolate(self, string):
def interpolate(self, string, keep_string=None):
try:
return self.templater(string).substitute(self.mapping)
return self.templater(string).substitute(self.mapping, keep_string)
except ValueError as e:
raise InvalidInterpolation(string, e)

Expand All @@ -66,12 +70,14 @@ class TemplateWithDefaults(string.Template):
idpattern = r'[_a-z][_a-z0-9]*(?::?-[^}]+)?'

# Modified from python2.7/string.py
def substitute(self, mapping):
def substitute(self, mapping, keep_string):
# 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 keep_string and named.startswith(keep_string):
return '$%s' % named
if ':-' in named:
var, _, default = named.partition(':-')
return mapping.get(var) or default
Expand Down
17 changes: 17 additions & 0 deletions Rake/Gemfile/molecule/model/schema_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
},
'command': {
'type': 'string',
'nullable': True,
},
}
},
Expand Down Expand Up @@ -571,6 +572,18 @@
},
}

dependency_command_nullable_schema = {
'dependency': {
'type': 'dict',
'schema': {
'command': {
'type': 'string',
'nullable': False,
},
}
},
}

verifier_options_readonly_schema = {
'verifier': {
'type': 'dict',
Expand Down Expand Up @@ -677,6 +690,10 @@ def _validate_disallowed(self, disallowed, field, value):
def validate(c):
schema = copy.deepcopy(base_schema)

# Dependency
if c['dependency']['name'] == 'shell':
util.merge_dicts(schema, dependency_command_nullable_schema)

# Driver
util.merge_dicts(schema, platforms_base_schema)
if c['driver']['name'] == 'docker':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import contextlib
import datetime
import os
import subprocess
import sys

import molecule
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
---
dependency:
name: shell
# TODO(retr0h): Setup the MOLECULE_ env vars prior to the
# interpolator.
# command: $MOLECULE_SCENARIO_DIRECTORY/run.bash
command: ./molecule/shell/run.bash
command: $MOLECULE_SCENARIO_DIRECTORY/run.bash
driver:
name: docker
# NOTE(retr0h): Required for functional tests, since
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ dependency:
driver:
# NOTE(retr0h): Functional test overrides this on command line.
name: vagrant
provider:
name: virtualbox
lint:
name: yamllint
options:
Expand Down
2 changes: 2 additions & 0 deletions Rake/Gemfile/test/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ def patched_testinfra(mocker):

@pytest.fixture
def patched_scenario_setup(mocker):
mocker.patch('molecule.config.Config.env')

return mocker.patch('molecule.scenario.Scenario._setup')


Expand Down
19 changes: 18 additions & 1 deletion Rake/Gemfile/test/unit/model/v2/test_dependency_section.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ def test_dependency_has_errors(_config):
'dependency': [{
'name': ['must be of string type'],
'enabled': ['must be of boolean type'],
'command': ['null value not allowed'],
'options': ['must be of dict type'],
'env': [{
'foo': ["value does not match regex '^[A-Z0-9_-]+$'"],
Expand Down Expand Up @@ -135,3 +134,21 @@ def test_dependency_invalid_dependency_name_has_errors(_config):
x = {'dependency': [{'name': ['unallowed value ']}]}

assert x == schema_v2.validate(_config)


@pytest.fixture
def _model_dependency_shell_errors_section_data():
return {
'dependency': {
'name': 'shell',
'command': None,
}
}


@pytest.mark.parametrize(
'_config', ['_model_dependency_shell_errors_section_data'], indirect=True)
def test_dependency_shell_has_errors(_config):
x = {'dependency': [{'command': ['null value not allowed']}]}

assert x == schema_v2.validate(_config)
18 changes: 10 additions & 8 deletions Rake/Gemfile/test/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,33 +406,37 @@ def test_get_driver_name_raises_when_different_driver_used(
patched_logger_critical.assert_called_once_with(msg)


def test_combine(config_instance):
assert isinstance(config_instance._combine(), dict)
def test_get_config(config_instance):
assert isinstance(config_instance._get_config(), dict)


def test_combine_with_base_config(config_instance):
def test_get_config_with_base_config(config_instance):
config_instance.args = {'base_config': './foo.yml'}
contents = {'foo': 'bar'}
util.write_file(config_instance.args['base_config'],
util.safe_dump(contents))
result = config_instance._combine()
result = config_instance._get_config()

assert result['foo'] == 'bar'


def test_reget_config(config_instance):
assert isinstance(config_instance._reget_config(), dict)


def test_interpolate(patched_logger_critical, config_instance):
string = 'foo: $HOME'
x = 'foo: {}'.format(os.environ['HOME'])

assert x == config_instance._interpolate(string)
assert x == config_instance._interpolate(string, os.environ, None)


def test_interpolate_raises_on_failed_interpolation(patched_logger_critical,
config_instance):
string = '$6$8I5Cfmpr$kGZB'

with pytest.raises(SystemExit) as e:
config_instance._interpolate(string)
config_instance._interpolate(string, os.environ, None)

assert 1 == e.value.code

Expand All @@ -453,8 +457,6 @@ def test_validate(mocker, config_instance, patched_logger_info,
patched_logger_info.assert_called_once_with(msg)

m.assert_called_once_with(config_instance.config)
assert 'ansible-galaxy' == config_instance.config['dependency']['command']
assert 'docker' == config_instance.config['driver']['name']

msg = 'Validation completed successfully.'
patched_logger_success.assert_called_once_with(msg)
Expand Down
Loading