diff --git a/compose/config/config.py b/compose/config/config.py index 11e89b09d..b79ef254d 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -13,7 +13,11 @@ from .errors import ( CircularReference, ComposeFileNotFound, ) -from .validation import validate_against_schema +from .validation import ( + validate_against_schema, + validate_service_names, + validate_top_level_object +) DOCKER_CONFIG_KEYS = [ @@ -122,19 +126,26 @@ def get_config_path(base_dir): return os.path.join(path, winner) +@validate_top_level_object +@validate_service_names +def pre_process_config(config): + """ + Pre validation checks and processing of the config file to interpolate env + vars returning a config dict ready to be tested against the schema. + """ + config = interpolate_environment_variables(config) + return config + + def load(config_details): config, working_dir, filename = config_details - if not isinstance(config, dict): - raise ConfigurationError( - "Top level object needs to be a dictionary. Check your .yml file that you have defined a service at the top level." - ) - config = interpolate_environment_variables(config) - validate_against_schema(config) + processed_config = pre_process_config(config) + validate_against_schema(processed_config) service_dicts = [] - for service_name, service_dict in list(config.items()): + for service_name, service_dict in list(processed_config.items()): loader = ServiceLoader(working_dir=working_dir, filename=filename) service_dict = loader.make_service_dict(service_name, service_dict) validate_paths(service_dict) diff --git a/compose/config/validation.py b/compose/config/validation.py index 36fd03b5f..26f3ca8ec 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -1,3 +1,4 @@ +from functools import wraps import os from docker.utils.ports import split_port @@ -36,6 +37,29 @@ def format_ports(instance): return True +def validate_service_names(func): + @wraps(func) + def func_wrapper(config): + for service_name in config.keys(): + if type(service_name) is int: + raise ConfigurationError( + "Service name: {} needs to be a string, eg '{}'".format(service_name, service_name) + ) + return func(config) + return func_wrapper + + +def validate_top_level_object(func): + @wraps(func) + def func_wrapper(config): + if not isinstance(config, dict): + raise ConfigurationError( + "Top level object needs to be a dictionary. Check your .yml file that you have defined a service at the top level." + ) + return func(config) + return func_wrapper + + def get_unsupported_config_msg(service_name, error_key): msg = "Unsupported config option for '{}' service: '{}'".format(service_name, error_key) if error_key in DOCKER_CONFIG_HINTS: diff --git a/tests/unit/config_test.py b/tests/unit/config_test.py index 3e3e9e34a..e61172562 100644 --- a/tests/unit/config_test.py +++ b/tests/unit/config_test.py @@ -64,6 +64,17 @@ class ConfigTest(unittest.TestCase): ) ) + def test_config_integer_service_name_raise_validation_error(self): + expected_error_msg = "Service name: 1 needs to be a string, eg '1'" + with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): + config.load( + config.ConfigDetails( + {1: {'image': 'busybox'}}, + 'working_dir', + 'filename.yml' + ) + ) + def test_config_valid_service_names(self): for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']: config.load(