Validate that each section of the config is a mapping before running interpolation.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2016-02-16 17:30:23 -05:00
parent 1f2c2942d7
commit 4b2a666231
5 changed files with 91 additions and 37 deletions

View File

@ -33,11 +33,11 @@ from .types import VolumeSpec
from .validation import match_named_volumes from .validation import match_named_volumes
from .validation import validate_against_fields_schema from .validation import validate_against_fields_schema
from .validation import validate_against_service_schema from .validation import validate_against_service_schema
from .validation import validate_config_section
from .validation import validate_depends_on from .validation import validate_depends_on
from .validation import validate_extends_file_path from .validation import validate_extends_file_path
from .validation import validate_network_mode from .validation import validate_network_mode
from .validation import validate_top_level_object from .validation import validate_top_level_object
from .validation import validate_top_level_service_objects
from .validation import validate_ulimits from .validation import validate_ulimits
@ -388,22 +388,31 @@ def load_services(working_dir, config_file, service_configs):
return build_services(service_config) return build_services(service_config)
def process_config_file(config_file, service_name=None): def interpolate_config_section(filename, config, section):
service_dicts = config_file.get_service_dicts() validate_config_section(filename, config, section)
validate_top_level_service_objects(config_file.filename, service_dicts) return interpolate_environment_variables(config, section)
interpolated_config = interpolate_environment_variables(service_dicts, 'service')
def process_config_file(config_file, service_name=None):
services = interpolate_config_section(
config_file.filename,
config_file.get_service_dicts(),
'service')
if config_file.version == V2_0: if config_file.version == V2_0:
processed_config = dict(config_file.config) processed_config = dict(config_file.config)
processed_config['services'] = services = interpolated_config processed_config['services'] = services
processed_config['volumes'] = interpolate_environment_variables( processed_config['volumes'] = interpolate_config_section(
config_file.get_volumes(), 'volume') config_file.filename,
processed_config['networks'] = interpolate_environment_variables( config_file.get_volumes(),
config_file.get_networks(), 'network') 'volume')
processed_config['networks'] = interpolate_config_section(
config_file.filename,
config_file.get_networks(),
'network')
if config_file.version == V1: if config_file.version == V1:
processed_config = services = interpolated_config processed_config = services
config_file = config_file._replace(config=processed_config) config_file = config_file._replace(config=processed_config)
validate_against_fields_schema(config_file) validate_against_fields_schema(config_file)

View File

@ -21,7 +21,7 @@ def interpolate_environment_variables(config, section):
) )
return dict( return dict(
(name, process_item(name, config_dict)) (name, process_item(name, config_dict or {}))
for name, config_dict in config.items() for name, config_dict in config.items()
) )

View File

@ -91,29 +91,50 @@ def match_named_volumes(service_dict, project_volumes):
) )
def validate_top_level_service_objects(filename, service_dicts): def python_type_to_yaml_type(type_):
"""Perform some high level validation of the service name and value. type_name = type(type_).__name__
return {
'dict': 'mapping',
'list': 'array',
'int': 'number',
'float': 'number',
'bool': 'boolean',
'unicode': 'string',
'str': 'string',
'bytes': 'string',
}.get(type_name, type_name)
This validation must happen before interpolation, which must happen
before the rest of validation, which is why it's separate from the def validate_config_section(filename, config, section):
rest of the service validation. """Validate the structure of a configuration section. This must be done
before interpolation so it's separate from schema validation.
""" """
for service_name, service_dict in service_dicts.items(): if not isinstance(config, dict):
if not isinstance(service_name, six.string_types): raise ConfigurationError(
raise ConfigurationError( "In file '{filename}' {section} must be a mapping, not "
"In file '{}' service name: {} needs to be a string, eg '{}'".format( "'{type}'.".format(
filename, filename=filename,
service_name, section=section,
service_name)) type=python_type_to_yaml_type(config)))
if not isinstance(service_dict, dict): for key, value in config.items():
if not isinstance(key, six.string_types):
raise ConfigurationError( raise ConfigurationError(
"In file '{}' service '{}' doesn\'t have any configuration options. " "In file '{filename}' {section} name {name} needs to be a "
"All top level keys in your docker-compose.yml must map " "string, eg '{name}'".format(
"to a dictionary of configuration options.".format( filename=filename,
filename, service_name section=section,
) name=key))
)
if not isinstance(value, (dict, type(None))):
raise ConfigurationError(
"In file '{filename}' {section} '{name}' is the wrong type. "
"It should be a mapping of configuration options, it is a "
"'{type}'.".format(
filename=filename,
section=section,
name=key,
type=python_type_to_yaml_type(value)))
def validate_top_level_object(config_file): def validate_top_level_object(config_file):

View File

@ -159,7 +159,7 @@ class CLITestCase(DockerClientTestCase):
'-f', 'tests/fixtures/invalid-composefile/invalid.yml', '-f', 'tests/fixtures/invalid-composefile/invalid.yml',
'config', '-q' 'config', '-q'
], returncode=1) ], returncode=1)
assert "'notaservice' doesn't have any configuration" in result.stderr assert "'notaservice' is the wrong type" in result.stderr
# TODO: this shouldn't be v2-dependent # TODO: this shouldn't be v2-dependent
@v2_only() @v2_only()

View File

@ -231,7 +231,7 @@ class ConfigTest(unittest.TestCase):
assert volumes['simple'] == {} assert volumes['simple'] == {}
assert volumes['other'] == {} assert volumes['other'] == {}
def test_volume_numeric_driver_opt(self): def test_named_volume_numeric_driver_opt(self):
config_details = build_config_details({ config_details = build_config_details({
'version': '2', 'version': '2',
'services': { 'services': {
@ -258,6 +258,30 @@ class ConfigTest(unittest.TestCase):
config.load(config_details) config.load(config_details)
assert 'driver_opts.size contains an invalid type' in exc.exconly() assert 'driver_opts.size contains an invalid type' in exc.exconly()
def test_named_volume_invalid_type_list(self):
config_details = build_config_details({
'version': '2',
'services': {
'simple': {'image': 'busybox'}
},
'volumes': []
})
with pytest.raises(ConfigurationError) as exc:
config.load(config_details)
assert "volume must be a mapping, not 'array'" in exc.exconly()
def test_networks_invalid_type_list(self):
config_details = build_config_details({
'version': '2',
'services': {
'simple': {'image': 'busybox'}
},
'networks': []
})
with pytest.raises(ConfigurationError) as exc:
config.load(config_details)
assert "network must be a mapping, not 'array'" in exc.exconly()
def test_load_service_with_name_version(self): def test_load_service_with_name_version(self):
with mock.patch('compose.config.config.log') as mock_logging: with mock.patch('compose.config.config.log') as mock_logging:
config_data = config.load( config_data = config.load(
@ -368,7 +392,7 @@ class ConfigTest(unittest.TestCase):
'filename.yml') 'filename.yml')
with pytest.raises(ConfigurationError) as exc: with pytest.raises(ConfigurationError) as exc:
config.load(config_details) config.load(config_details)
error_msg = "service 'web' doesn't have any configuration options" error_msg = "service 'web' is the wrong type"
assert error_msg in exc.exconly() assert error_msg in exc.exconly()
def test_config_integer_service_name_raise_validation_error(self): def test_config_integer_service_name_raise_validation_error(self):
@ -381,7 +405,7 @@ class ConfigTest(unittest.TestCase):
) )
) )
assert "In file 'filename.yml' service name: 1 needs to be a string, eg '1'" \ assert "In file 'filename.yml' service name 1 needs to be a string, eg '1'" \
in excinfo.exconly() in excinfo.exconly()
def test_config_integer_service_name_raise_validation_error_v2(self): def test_config_integer_service_name_raise_validation_error_v2(self):
@ -397,7 +421,7 @@ class ConfigTest(unittest.TestCase):
) )
) )
assert "In file 'filename.yml' service name: 1 needs to be a string, eg '1'" \ assert "In file 'filename.yml' service name 1 needs to be a string, eg '1'" \
in excinfo.exconly() in excinfo.exconly()
def test_load_with_multiple_files_v1(self): def test_load_with_multiple_files_v1(self):
@ -532,7 +556,7 @@ class ConfigTest(unittest.TestCase):
with pytest.raises(ConfigurationError) as exc: with pytest.raises(ConfigurationError) as exc:
config.load(details) config.load(details)
assert "service 'bogus' doesn't have any configuration" in exc.exconly() assert "service 'bogus' is the wrong type" in exc.exconly()
assert "In file 'override.yaml'" in exc.exconly() assert "In file 'override.yaml'" in exc.exconly()
def test_load_sorts_in_dependency_order(self): def test_load_sorts_in_dependency_order(self):