Improve error messages for invalid versions

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
This commit is contained in:
Aanand Prasad 2016-01-29 16:15:16 +00:00
parent ce0d469c18
commit f081376067
5 changed files with 71 additions and 42 deletions

View File

@ -16,10 +16,10 @@ from cached_property import cached_property
from ..const import COMPOSEFILE_V1 as V1 from ..const import COMPOSEFILE_V1 as V1
from ..const import COMPOSEFILE_V2_0 as V2_0 from ..const import COMPOSEFILE_V2_0 as V2_0
from ..const import COMPOSEFILE_VERSIONS
from .errors import CircularReference from .errors import CircularReference
from .errors import ComposeFileNotFound from .errors import ComposeFileNotFound
from .errors import ConfigurationError from .errors import ConfigurationError
from .errors import VERSION_EXPLANATION
from .interpolation import interpolate_environment_variables from .interpolation import interpolate_environment_variables
from .sort_services import get_container_name_from_network_mode from .sort_services import get_container_name_from_network_mode
from .sort_services import get_service_name_from_network_mode from .sort_services import get_service_name_from_network_mode
@ -105,6 +105,7 @@ SUPPORTED_FILENAMES = [
DEFAULT_OVERRIDE_FILENAME = 'docker-compose.override.yml' DEFAULT_OVERRIDE_FILENAME = 'docker-compose.override.yml'
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -131,7 +132,10 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
@cached_property @cached_property
def version(self): def version(self):
version = self.config.get('version', V1) if 'version' not in self.config:
return V1
version = self.config['version']
if isinstance(version, dict): if isinstance(version, dict):
log.warn("Unexpected type for field 'version', in file {} assuming " log.warn("Unexpected type for field 'version', in file {} assuming "
@ -139,12 +143,23 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
"Compose file version 1".format(self.filename)) "Compose file version 1".format(self.filename))
return V1 return V1
if not isinstance(version, six.string_types):
raise ConfigurationError(
'Version in "{}" is invalid - it should be a string.'
.format(self.filename))
if version == '1':
raise ConfigurationError(
'Version in "{}" is invalid. {}'
.format(self.filename, VERSION_EXPLANATION))
if version == '2': if version == '2':
version = V2_0 version = V2_0
if version not in COMPOSEFILE_VERSIONS: if version != V2_0:
raise ConfigurationError( raise ConfigurationError(
'Invalid Compose file version: {0}'.format(version)) 'Version in "{}" is unsupported. {}'
.format(self.filename, VERSION_EXPLANATION))
return version return version

View File

@ -2,6 +2,14 @@ from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
VERSION_EXPLANATION = (
'Either specify a version of "2" (or "2.0") and place your service '
'definitions under the `services` key, or omit the `version` key and place '
'your service definitions at the root of the file to use version 1.\n'
'For more on the Compose file format versions, see '
'https://docs.docker.com/compose/compose-file/')
class ConfigurationError(Exception): class ConfigurationError(Exception):
def __init__(self, msg): def __init__(self, msg):
self.msg = msg self.msg = msg

View File

@ -15,6 +15,7 @@ from jsonschema import RefResolver
from jsonschema import ValidationError from jsonschema import ValidationError
from .errors import ConfigurationError from .errors import ConfigurationError
from .errors import VERSION_EXPLANATION
from .sort_services import get_service_name_from_network_mode from .sort_services import get_service_name_from_network_mode
@ -229,11 +230,14 @@ def handle_error_for_schema_with_id(error, path):
"A service can either be built to image or use an existing " "A service can either be built to image or use an existing "
"image, not both.".format(path_string(path))) "image, not both.".format(path_string(path)))
if schema_id == '#/definitions/service': if error.validator == 'additionalProperties':
if error.validator == 'additionalProperties': if schema_id == '#/definitions/service':
invalid_config_key = parse_key_from_error_msg(error) invalid_config_key = parse_key_from_error_msg(error)
return get_unsupported_config_msg(path, invalid_config_key) return get_unsupported_config_msg(path, invalid_config_key)
if not error.path:
return '{}\n{}'.format(error.message, VERSION_EXPLANATION)
def handle_generic_service_error(error, path): def handle_generic_service_error(error, path):
msg_format = None msg_format = None

View File

@ -17,7 +17,6 @@ LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
COMPOSEFILE_V1 = '1' COMPOSEFILE_V1 = '1'
COMPOSEFILE_V2_0 = '2.0' COMPOSEFILE_V2_0 = '2.0'
COMPOSEFILE_VERSIONS = (COMPOSEFILE_V1, COMPOSEFILE_V2_0)
API_VERSIONS = { API_VERSIONS = {
COMPOSEFILE_V1: '1.21', COMPOSEFILE_V1: '1.21',

View File

@ -17,6 +17,7 @@ from compose.config.config import resolve_environment
from compose.config.config import V1 from compose.config.config import V1
from compose.config.config import V2_0 from compose.config.config import V2_0
from compose.config.errors import ConfigurationError from compose.config.errors import ConfigurationError
from compose.config.errors import VERSION_EXPLANATION
from compose.config.types import VolumeSpec from compose.config.types import VolumeSpec
from compose.const import IS_WINDOWS_PLATFORM from compose.const import IS_WINDOWS_PLATFORM
from tests import mock from tests import mock
@ -159,8 +160,8 @@ class ConfigTest(unittest.TestCase):
assert list(s['name'] for s in cfg.services) == ['version'] assert list(s['name'] for s in cfg.services) == ['version']
def test_wrong_version_type(self): def test_wrong_version_type(self):
for version in [None, 2, 2.0]: for version in [None, 1, 2, 2.0]:
with pytest.raises(ConfigurationError): with pytest.raises(ConfigurationError) as excinfo:
config.load( config.load(
build_config_details( build_config_details(
{'version': version}, {'version': version},
@ -168,8 +169,11 @@ class ConfigTest(unittest.TestCase):
) )
) )
assert 'Version in "filename.yml" is invalid - it should be a string.' \
in excinfo.exconly()
def test_unsupported_version(self): def test_unsupported_version(self):
with pytest.raises(ConfigurationError): with pytest.raises(ConfigurationError) as excinfo:
config.load( config.load(
build_config_details( build_config_details(
{'version': '2.1'}, {'version': '2.1'},
@ -177,18 +181,38 @@ class ConfigTest(unittest.TestCase):
) )
) )
def test_v1_file_with_version_is_invalid(self): assert 'Version in "filename.yml" is unsupported' in excinfo.exconly()
for version in [1, "1"]: assert VERSION_EXPLANATION in excinfo.exconly()
with pytest.raises(ConfigurationError):
config.load( def test_version_1_is_invalid(self):
build_config_details( with pytest.raises(ConfigurationError) as excinfo:
{ config.load(
'version': version, build_config_details(
'web': {'image': 'busybox'}, {
}, 'version': '1',
filename='filename.yml', 'web': {'image': 'busybox'},
) },
filename='filename.yml',
) )
)
assert 'Version in "filename.yml" is invalid' in excinfo.exconly()
assert VERSION_EXPLANATION in excinfo.exconly()
def test_v1_file_with_version_is_invalid(self):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
'version': '2',
'web': {'image': 'busybox'},
},
filename='filename.yml',
)
)
assert 'Additional properties are not allowed' in excinfo.exconly()
assert VERSION_EXPLANATION in excinfo.exconly()
def test_named_volume_config_empty(self): def test_named_volume_config_empty(self):
config_details = build_config_details({ config_details = build_config_details({
@ -226,27 +250,6 @@ class ConfigTest(unittest.TestCase):
]) ])
) )
def test_load_invalid_version(self):
with self.assertRaises(ConfigurationError):
config.load(
build_config_details({
'version': 18,
'services': {
'foo': {'image': 'busybox'}
}
}, 'working_dir', 'filename.yml')
)
with self.assertRaises(ConfigurationError):
config.load(
build_config_details({
'version': 'two point oh',
'services': {
'foo': {'image': 'busybox'}
}
}, 'working_dir', 'filename.yml')
)
def test_load_throws_error_when_not_dict(self): def test_load_throws_error_when_not_dict(self):
with self.assertRaises(ConfigurationError): with self.assertRaises(ConfigurationError):
config.load( config.load(