Merge pull request #2783 from aanand/fix-validation-v2

Fix validation and version checking
This commit is contained in:
Aanand Prasad 2016-02-02 15:19:50 +00:00
commit 60a5b39f6f
29 changed files with 414 additions and 252 deletions

View File

@ -14,10 +14,12 @@ import six
import yaml
from cached_property import cached_property
from ..const import COMPOSEFILE_VERSIONS
from ..const import COMPOSEFILE_V1 as V1
from ..const import COMPOSEFILE_V2_0 as V2_0
from .errors import CircularReference
from .errors import ComposeFileNotFound
from .errors import ConfigurationError
from .errors import VERSION_EXPLANATION
from .interpolation import interpolate_environment_variables
from .sort_services import get_container_name_from_network_mode
from .sort_services import get_service_name_from_network_mode
@ -103,6 +105,7 @@ SUPPORTED_FILENAMES = [
DEFAULT_OVERRIDE_FILENAME = 'docker-compose.override.yml'
log = logging.getLogger(__name__)
@ -129,27 +132,48 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
@cached_property
def version(self):
if self.config is None:
return 1
version = self.config.get('version', 1)
if 'version' not in self.config:
return V1
version = self.config['version']
if isinstance(version, dict):
log.warn("Unexpected type for field 'version', in file {} assuming "
"version is the name of a service, and defaulting to "
"Compose file version 1".format(self.filename))
return 1
log.warn('Unexpected type for "version" key in "{}". Assuming '
'"version" is the name of a service, and defaulting to '
'Compose file version 1.'.format(self.filename))
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':
version = V2_0
if version != V2_0:
raise ConfigurationError(
'Version in "{}" is unsupported. {}'
.format(self.filename, VERSION_EXPLANATION))
return version
def get_service(self, name):
return self.get_service_dicts()[name]
def get_service_dicts(self):
return self.config if self.version == 1 else self.config.get('services', {})
return self.config if self.version == V1 else self.config.get('services', {})
def get_volumes(self):
return {} if self.version == 1 else self.config.get('volumes', {})
return {} if self.version == V1 else self.config.get('volumes', {})
def get_networks(self):
return {} if self.version == 1 else self.config.get('networks', {})
return {} if self.version == V1 else self.config.get('networks', {})
class Config(namedtuple('_Config', 'version services volumes networks')):
@ -211,10 +235,6 @@ def validate_config_version(config_files):
next_file.filename,
next_file.version))
if main_file.version not in COMPOSEFILE_VERSIONS:
raise ConfigurationError(
'Invalid Compose file version: {0}'.format(main_file.version))
def get_default_config_files(base_dir):
(candidates, path) = find_candidates_in_parent_dirs(SUPPORTED_FILENAMES, base_dir)
@ -278,7 +298,7 @@ def load(config_details):
main_file,
[file.get_service_dicts() for file in config_details.config_files])
if main_file.version >= 2:
if main_file.version != V1:
for service_dict in service_dicts:
match_named_volumes(service_dict, volumes)
@ -363,7 +383,7 @@ def process_config_file(config_file, service_name=None):
interpolated_config = interpolate_environment_variables(service_dicts, 'service')
if config_file.version == 2:
if config_file.version == V2_0:
processed_config = dict(config_file.config)
processed_config['services'] = services = interpolated_config
processed_config['volumes'] = interpolate_environment_variables(
@ -371,7 +391,7 @@ def process_config_file(config_file, service_name=None):
processed_config['networks'] = interpolate_environment_variables(
config_file.get_networks(), 'network')
if config_file.version == 1:
if config_file.version == V1:
processed_config = services = interpolated_config
config_file = config_file._replace(config=processed_config)
@ -655,7 +675,7 @@ def merge_service_dicts(base, override, version):
if field in base or field in override:
d[field] = override.get(field, base.get(field))
if version == 1:
if version == V1:
legacy_v1_merge_image_or_build(d, base, override)
else:
merge_build(d, base, override)

View File

@ -2,6 +2,14 @@ from __future__ import absolute_import
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):
def __init__(self, msg):
self.msg = msg

View File

@ -1,18 +1,18 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"id": "fields_schema_v2.json",
"id": "fields_schema_v2.0.json",
"properties": {
"version": {
"enum": [2]
"type": "string"
},
"services": {
"id": "#/properties/services",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "service_schema_v2.json#/definitions/service"
"$ref": "service_schema_v2.0.json#/definitions/service"
}
},
"additionalProperties": false

View File

@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "service_schema_v2.json",
"id": "service_schema_v2.0.json",
"type": "object",

View File

@ -7,6 +7,7 @@ from __future__ import unicode_literals
import os
from collections import namedtuple
from compose.config.config import V1
from compose.config.errors import ConfigurationError
from compose.const import IS_WINDOWS_PLATFORM
@ -16,7 +17,7 @@ class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')):
# TODO: drop service_names arg when v1 is removed
@classmethod
def parse(cls, volume_from_config, service_names, version):
func = cls.parse_v1 if version == 1 else cls.parse_v2
func = cls.parse_v1 if version == V1 else cls.parse_v2
return func(service_names, volume_from_config)
@classmethod

View File

@ -15,6 +15,7 @@ from jsonschema import RefResolver
from jsonschema import ValidationError
from .errors import ConfigurationError
from .errors import VERSION_EXPLANATION
from .sort_services import get_service_name_from_network_mode
@ -174,8 +175,8 @@ def validate_depends_on(service_config, service_names):
"undefined.".format(s=service_config, dep=dependency))
def get_unsupported_config_msg(service_name, error_key):
msg = "Unsupported config option for '{}' service: '{}'".format(service_name, error_key)
def get_unsupported_config_msg(path, error_key):
msg = "Unsupported config option for {}: '{}'".format(path_string(path), error_key)
if error_key in DOCKER_CONFIG_HINTS:
msg += " (did you mean '{}'?)".format(DOCKER_CONFIG_HINTS[error_key])
return msg
@ -191,7 +192,7 @@ def is_service_dict_schema(schema_id):
return schema_id == 'fields_schema_v1.json' or schema_id == '#/properties/services'
def handle_error_for_schema_with_id(error, service_name):
def handle_error_for_schema_with_id(error, path):
schema_id = error.schema['id']
if is_service_dict_schema(schema_id) and error.validator == 'additionalProperties':
@ -215,62 +216,67 @@ def handle_error_for_schema_with_id(error, service_name):
# TODO: only applies to v1
if 'image' in error.instance and context:
return (
"Service '{}' has both an image and build path specified. "
"{} has both an image and build path specified. "
"A service can either be built to image or use an existing "
"image, not both.".format(service_name))
"image, not both.".format(path_string(path)))
if 'image' not in error.instance and not context:
return (
"Service '{}' has neither an image nor a build path "
"specified. At least one must be provided.".format(service_name))
"{} has neither an image nor a build path specified. "
"At least one must be provided.".format(path_string(path)))
# TODO: only applies to v1
if 'image' in error.instance and dockerfile:
return (
"Service '{}' has both an image and alternate Dockerfile. "
"{} has both an image and alternate Dockerfile. "
"A service can either be built to image or use an existing "
"image, not both.".format(service_name))
"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)
return get_unsupported_config_msg(service_name, 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, service_name):
config_key = " ".join("'%s'" % k for k in error.path)
def handle_generic_service_error(error, path):
msg_format = None
error_msg = error.message
if error.validator == 'oneOf':
msg_format = "Service '{}' configuration key {} {}"
error_msg = _parse_oneof_validator(error)
msg_format = "{path} {msg}"
config_key, error_msg = _parse_oneof_validator(error)
if config_key:
path.append(config_key)
elif error.validator == 'type':
msg_format = ("Service '{}' configuration key {} contains an invalid "
"type, it should be {}")
msg_format = "{path} contains an invalid type, it should be {msg}"
error_msg = _parse_valid_types_from_validator(error.validator_value)
# TODO: no test case for this branch, there are no config options
# which exercise this branch
elif error.validator == 'required':
msg_format = "Service '{}' configuration key '{}' is invalid, {}"
msg_format = "{path} is invalid, {msg}"
elif error.validator == 'dependencies':
msg_format = "Service '{}' configuration key '{}' is invalid: {}"
config_key = list(error.validator_value.keys())[0]
required_keys = ",".join(error.validator_value[config_key])
msg_format = "{path} is invalid: {msg}"
path.append(config_key)
error_msg = "when defining '{}' you must set '{}' as well".format(
config_key,
required_keys)
elif error.cause:
error_msg = six.text_type(error.cause)
msg_format = "Service '{}' configuration key {} is invalid: {}"
msg_format = "{path} is invalid: {msg}"
elif error.path:
msg_format = "Service '{}' configuration key {} value {}"
msg_format = "{path} value {msg}"
if msg_format:
return msg_format.format(service_name, config_key, error_msg)
return msg_format.format(path=path_string(path), msg=error_msg)
return error.message
@ -279,6 +285,10 @@ def parse_key_from_error_msg(error):
return error.message.split("'")[1]
def path_string(path):
return ".".join(c for c in path if isinstance(c, six.string_types))
def _parse_valid_types_from_validator(validator):
"""A validator value can be either an array of valid types or a string of
a valid type. Parse the valid types and prefix with the correct article.
@ -304,52 +314,52 @@ def _parse_oneof_validator(error):
for context in error.context:
if context.validator == 'required':
return context.message
return (None, context.message)
if context.validator == 'additionalProperties':
invalid_config_key = parse_key_from_error_msg(context)
return "contains unsupported option: '{}'".format(invalid_config_key)
return (None, "contains unsupported option: '{}'".format(invalid_config_key))
if context.path:
invalid_config_key = " ".join(
"'{}' ".format(fragment) for fragment in context.path
if isinstance(fragment, six.string_types)
return (
path_string(context.path),
"contains {}, which is an invalid type, it should be {}".format(
json.dumps(context.instance),
_parse_valid_types_from_validator(context.validator_value)),
)
return "{}contains {}, which is an invalid type, it should be {}".format(
invalid_config_key,
# Always print the json repr of the invalid value
json.dumps(context.instance),
_parse_valid_types_from_validator(context.validator_value))
if context.validator == 'uniqueItems':
return "contains non unique items, please remove duplicates from {}".format(
context.instance)
return (
None,
"contains non unique items, please remove duplicates from {}".format(
context.instance),
)
if context.validator == 'type':
types.append(context.validator_value)
valid_types = _parse_valid_types_from_validator(types)
return "contains an invalid type, it should be {}".format(valid_types)
return (None, "contains an invalid type, it should be {}".format(valid_types))
def process_errors(errors, service_name=None):
def process_errors(errors, path_prefix=None):
"""jsonschema gives us an error tree full of information to explain what has
gone wrong. Process each error and pull out relevant information and re-write
helpful error messages that are relevant.
"""
def format_error_message(error, service_name):
if not service_name and error.path:
# field_schema errors will have service name on the path
service_name = error.path.popleft()
path_prefix = path_prefix or []
def format_error_message(error):
path = path_prefix + list(error.path)
if 'id' in error.schema:
error_msg = handle_error_for_schema_with_id(error, service_name)
error_msg = handle_error_for_schema_with_id(error, path)
if error_msg:
return error_msg
return handle_generic_service_error(error, service_name)
return handle_generic_service_error(error, path)
return '\n'.join(format_error_message(error, service_name) for error in errors)
return '\n'.join(format_error_message(error) for error in errors)
def validate_against_fields_schema(config_file):
@ -366,14 +376,14 @@ def validate_against_service_schema(config, service_name, version):
config,
"service_schema_v{0}.json".format(version),
format_checker=["ports"],
service_name=service_name)
path_prefix=[service_name])
def _validate_against_schema(
config,
schema_filename,
format_checker=(),
service_name=None,
path_prefix=None,
filename=None):
config_source_dir = os.path.dirname(os.path.abspath(__file__))
@ -399,7 +409,7 @@ def _validate_against_schema(
if not errors:
return
error_msg = process_errors(errors, service_name)
error_msg = process_errors(errors, path_prefix=path_prefix)
file_msg = " in file '{}'".format(filename) if filename else ''
raise ConfigurationError("Validation failed{}, reason(s):\n{}".format(
file_msg,

View File

@ -14,9 +14,11 @@ LABEL_PROJECT = 'com.docker.compose.project'
LABEL_SERVICE = 'com.docker.compose.service'
LABEL_VERSION = 'com.docker.compose.version'
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
COMPOSEFILE_VERSIONS = (1, 2)
COMPOSEFILE_V1 = '1'
COMPOSEFILE_V2_0 = '2.0'
API_VERSIONS = {
1: '1.21',
2: '1.22',
COMPOSEFILE_V1: '1.21',
COMPOSEFILE_V2_0: '1.22',
}

View File

@ -10,6 +10,7 @@ from docker.errors import NotFound
from . import parallel
from .config import ConfigurationError
from .config.config import V1
from .config.sort_services import get_container_name_from_network_mode
from .config.sort_services import get_service_name_from_network_mode
from .const import DEFAULT_TIMEOUT
@ -56,7 +57,7 @@ class Project(object):
"""
Construct a Project from a config.Config object.
"""
use_networking = (config_data.version and config_data.version >= 2)
use_networking = (config_data.version and config_data.version != V1)
project = cls(name, [], client, use_networking=use_networking)
network_config = config_data.networks or {}
@ -94,7 +95,7 @@ class Project(object):
network_mode = project.get_network_mode(service_dict, networks)
volumes_from = get_volumes_from(project, service_dict)
if config_data.version == 2:
if config_data.version != V1:
service_volumes = service_dict.get('volumes', [])
for volume_spec in service_volumes:
if volume_spec.is_named_volume:

View File

@ -23,8 +23,8 @@ exe = EXE(pyz,
'DATA'
),
(
'compose/config/fields_schema_v2.json',
'compose/config/fields_schema_v2.json',
'compose/config/fields_schema_v2.0.json',
'compose/config/fields_schema_v2.0.json',
'DATA'
),
(
@ -33,8 +33,8 @@ exe = EXE(pyz,
'DATA'
),
(
'compose/config/service_schema_v2.json',
'compose/config/service_schema_v2.json',
'compose/config/service_schema_v2.0.json',
'compose/config/service_schema_v2.0.json',
'DATA'
),
(

View File

@ -177,7 +177,7 @@ class CLITestCase(DockerClientTestCase):
output = yaml.load(result.stdout)
expected = {
'version': 2,
'version': '2.0',
'volumes': {'data': {'driver': 'local'}},
'networks': {'front': {}},
'services': {

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
services:
myweb:
build: '.'

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
services:
simple:
image: busybox:latest

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
services:
foo:

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
services:
web:

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
services:
simple:
image: busybox:latest

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
services:
web:

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
services:
simple:
image: busybox:latest

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
services:
web:

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
services:
web:

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
services:
bridge:

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
networks:
foo: {}

View File

@ -1,5 +1,5 @@
version: 2
version: "2"
services:
simple:

View File

@ -1,5 +1,5 @@
version: 2
version: "2"
volumes:
data:

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
services:
simple:
image: busybox:latest

View File

@ -1,4 +1,4 @@
version: 2
version: "2"
services:
simple:
image: busybox:latest

View File

@ -10,6 +10,7 @@ from docker.errors import NotFound
from .testcases import DockerClientTestCase
from compose.config import config
from compose.config import ConfigurationError
from compose.config.config import V2_0
from compose.config.types import VolumeFromSpec
from compose.config.types import VolumeSpec
from compose.const import LABEL_PROJECT
@ -112,7 +113,7 @@ class ProjectTest(DockerClientTestCase):
name='composetest',
client=self.client,
config_data=build_service_dicts({
'version': 2,
'version': V2_0,
'services': {
'net': {
'image': 'busybox:latest',
@ -139,7 +140,7 @@ class ProjectTest(DockerClientTestCase):
return Project.from_config(
name='composetest',
config_data=build_service_dicts({
'version': 2,
'version': V2_0,
'services': {
'web': {
'image': 'busybox:latest',
@ -559,7 +560,7 @@ class ProjectTest(DockerClientTestCase):
@v2_only()
def test_project_up_networks(self):
config_data = config.Config(
version=2,
version=V2_0,
services=[{
'name': 'web',
'image': 'busybox:latest',
@ -592,7 +593,7 @@ class ProjectTest(DockerClientTestCase):
@v2_only()
def test_up_with_ipam_config(self):
config_data = config.Config(
version=2,
version=V2_0,
services=[],
volumes={},
networks={
@ -651,7 +652,7 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config(
version=2,
version=V2_0,
services=[{
'name': 'web',
'image': 'busybox:latest',
@ -677,7 +678,7 @@ class ProjectTest(DockerClientTestCase):
base_file = config.ConfigFile(
'base.yml',
{
'version': 2,
'version': V2_0,
'services': {
'simple': {'image': 'busybox:latest', 'command': 'top'},
'another': {
@ -696,7 +697,7 @@ class ProjectTest(DockerClientTestCase):
override_file = config.ConfigFile(
'override.yml',
{
'version': 2,
'version': V2_0,
'services': {
'another': {
'logging': {
@ -729,7 +730,7 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config(
version=2,
version=V2_0,
services=[{
'name': 'web',
'image': 'busybox:latest',
@ -754,7 +755,7 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config(
version=2,
version=V2_0,
services=[{
'name': 'web',
'image': 'busybox:latest',
@ -779,7 +780,7 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32))
config_data = config.Config(
version=2,
version=V2_0,
services=[{
'name': 'web',
'image': 'busybox:latest',
@ -802,7 +803,7 @@ class ProjectTest(DockerClientTestCase):
full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config(
version=2,
version=V2_0,
services=[{
'name': 'web',
'image': 'busybox:latest',
@ -841,7 +842,7 @@ class ProjectTest(DockerClientTestCase):
full_vol_name = 'composetest_{0}'.format(vol_name)
self.client.create_volume(vol_name)
config_data = config.Config(
version=2,
version=V2_0,
services=[{
'name': 'web',
'image': 'busybox:latest',
@ -866,7 +867,7 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32))
config_data = config.Config(
version=2,
version=V2_0,
services=[{
'name': 'web',
'image': 'busybox:latest',
@ -895,7 +896,7 @@ class ProjectTest(DockerClientTestCase):
base_file = config.ConfigFile(
'base.yml',
{
'version': 2,
'version': V2_0,
'services': {
'simple': {
'image': 'busybox:latest',

View File

@ -10,6 +10,8 @@ from pytest import skip
from .. import unittest
from compose.cli.docker_client import docker_client
from compose.config.config import resolve_environment
from compose.config.config import V1
from compose.config.config import V2_0
from compose.const import API_VERSIONS
from compose.const import LABEL_PROJECT
from compose.progress_stream import stream_output
@ -54,9 +56,9 @@ class DockerClientTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
if engine_version_too_low_for_v2():
version = API_VERSIONS[1]
version = API_VERSIONS[V1]
else:
version = API_VERSIONS[2]
version = API_VERSIONS[V2_0]
cls.client = docker_client(version)

View File

@ -14,14 +14,16 @@ import pytest
from compose.config import config
from compose.config.config import resolve_build_args
from compose.config.config import resolve_environment
from compose.config.config import V1
from compose.config.config import V2_0
from compose.config.errors import ConfigurationError
from compose.config.errors import VERSION_EXPLANATION
from compose.config.types import VolumeSpec
from compose.const import IS_WINDOWS_PLATFORM
from tests import mock
from tests import unittest
DEFAULT_VERSION = V2 = 2
V1 = 1
DEFAULT_VERSION = V2_0
def make_service_dict(name, service_dict, working_dir, filename=None):
@ -78,7 +80,7 @@ class ConfigTest(unittest.TestCase):
def test_load_v2(self):
config_data = config.load(
build_config_details({
'version': 2,
'version': '2',
'services': {
'foo': {'image': 'busybox'},
'bar': {'image': 'busybox', 'environment': ['FOO=1']},
@ -143,9 +145,78 @@ class ConfigTest(unittest.TestCase):
}
})
def test_valid_versions(self):
for version in ['2', '2.0']:
cfg = config.load(build_config_details({'version': version}))
assert cfg.version == V2_0
def test_v1_file_version(self):
cfg = config.load(build_config_details({'web': {'image': 'busybox'}}))
assert cfg.version == V1
assert list(s['name'] for s in cfg.services) == ['web']
cfg = config.load(build_config_details({'version': {'image': 'busybox'}}))
assert cfg.version == V1
assert list(s['name'] for s in cfg.services) == ['version']
def test_wrong_version_type(self):
for version in [None, 1, 2, 2.0]:
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{'version': version},
filename='filename.yml',
)
)
assert 'Version in "filename.yml" is invalid - it should be a string.' \
in excinfo.exconly()
def test_unsupported_version(self):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{'version': '2.1'},
filename='filename.yml',
)
)
assert 'Version in "filename.yml" is unsupported' in excinfo.exconly()
assert VERSION_EXPLANATION in excinfo.exconly()
def test_version_1_is_invalid(self):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
'version': '1',
'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):
config_details = build_config_details({
'version': 2,
'version': '2',
'services': {
'simple': {'image': 'busybox'}
},
@ -161,13 +232,18 @@ class ConfigTest(unittest.TestCase):
assert volumes['other'] == {}
def test_load_service_with_name_version(self):
config_data = config.load(
build_config_details({
'version': {
'image': 'busybox'
}
}, 'working_dir', 'filename.yml')
)
with mock.patch('compose.config.config.log') as mock_logging:
config_data = config.load(
build_config_details({
'version': {
'image': 'busybox'
}
}, 'working_dir', 'filename.yml')
)
assert 'Unexpected type for "version" key in "filename.yml"' \
in mock_logging.warn.call_args[0][0]
service_dicts = config_data.services
self.assertEqual(
service_sort(service_dicts),
@ -179,27 +255,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):
with self.assertRaises(ConfigurationError):
config.load(
@ -214,7 +269,7 @@ class ConfigTest(unittest.TestCase):
with self.assertRaises(ConfigurationError):
config.load(
build_config_details(
{'version': 2, 'services': {'web': 'busybox:latest'}},
{'version': '2', 'services': {'web': 'busybox:latest'}},
'working_dir',
'filename.yml'
)
@ -224,7 +279,7 @@ class ConfigTest(unittest.TestCase):
with self.assertRaises(ConfigurationError):
config.load(
build_config_details({
'version': 2,
'version': '2',
'services': {'web': 'busybox:latest'},
'networks': {
'invalid': {'foo', 'bar'}
@ -246,22 +301,38 @@ class ConfigTest(unittest.TestCase):
with pytest.raises(ConfigurationError) as exc:
config.load(
build_config_details({
'version': 2,
'version': '2',
'services': {invalid_name: {'image': 'busybox'}}
}, 'working_dir', 'filename.yml')
)
assert 'Invalid service name \'%s\'' % invalid_name in exc.exconly()
def test_load_with_invalid_field_name(self):
config_details = build_config_details(
{'web': {'image': 'busybox', 'name': 'bogus'}},
'working_dir',
'filename.yml')
with pytest.raises(ConfigurationError) as exc:
config.load(config_details)
error_msg = "Unsupported config option for 'web' service: 'name'"
assert error_msg in exc.exconly()
assert "Validation failed in file 'filename.yml'" in exc.exconly()
config.load(build_config_details(
{
'version': '2',
'services': {
'web': {'image': 'busybox', 'name': 'bogus'},
}
},
'working_dir',
'filename.yml',
))
assert "Unsupported config option for services.web: 'name'" in exc.exconly()
def test_load_with_invalid_field_name_v1(self):
with pytest.raises(ConfigurationError) as exc:
config.load(build_config_details(
{
'web': {'image': 'busybox', 'name': 'bogus'},
},
'working_dir',
'filename.yml',
))
assert "Unsupported config option for web: 'name'" in exc.exconly()
def test_load_invalid_service_definition(self):
config_details = build_config_details(
@ -274,9 +345,7 @@ class ConfigTest(unittest.TestCase):
assert error_msg in exc.exconly()
def test_config_integer_service_name_raise_validation_error(self):
expected_error_msg = ("In file 'filename.yml' service name: 1 needs to "
"be a string, eg '1'")
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{1: {'image': 'busybox'}},
@ -285,15 +354,15 @@ class ConfigTest(unittest.TestCase):
)
)
def test_config_integer_service_name_raise_validation_error_v2(self):
expected_error_msg = ("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()
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
def test_config_integer_service_name_raise_validation_error_v2(self):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
'version': 2,
'version': '2',
'services': {1: {'image': 'busybox'}}
},
'working_dir',
@ -301,6 +370,9 @@ class ConfigTest(unittest.TestCase):
)
)
assert "In file 'filename.yml' service name: 1 needs to be a string, eg '1'" \
in excinfo.exconly()
def test_load_with_multiple_files_v1(self):
base_file = config.ConfigFile(
'base.yaml',
@ -353,7 +425,7 @@ class ConfigTest(unittest.TestCase):
def test_load_with_multiple_files_and_empty_override_v2(self):
base_file = config.ConfigFile(
'base.yml',
{'version': 2, 'services': {'web': {'image': 'example/web'}}})
{'version': '2', 'services': {'web': {'image': 'example/web'}}})
override_file = config.ConfigFile('override.yml', None)
details = config.ConfigDetails('.', [base_file, override_file])
@ -377,7 +449,7 @@ class ConfigTest(unittest.TestCase):
base_file = config.ConfigFile('base.yml', None)
override_file = config.ConfigFile(
'override.tml',
{'version': 2, 'services': {'web': {'image': 'example/web'}}}
{'version': '2', 'services': {'web': {'image': 'example/web'}}}
)
details = config.ConfigDetails('.', [base_file, override_file])
with pytest.raises(ConfigurationError) as exc:
@ -477,7 +549,7 @@ class ConfigTest(unittest.TestCase):
config.load(
build_config_details(
{
'version': 2,
'version': '2',
'services': {
'web': {
'build': '.',
@ -492,7 +564,7 @@ class ConfigTest(unittest.TestCase):
service = config.load(
build_config_details({
'version': 2,
'version': '2',
'services': {
'web': {
'build': '.'
@ -505,7 +577,7 @@ class ConfigTest(unittest.TestCase):
service = config.load(
build_config_details(
{
'version': 2,
'version': '2',
'services': {
'web': {
'build': {
@ -526,7 +598,7 @@ class ConfigTest(unittest.TestCase):
base_file = config.ConfigFile(
'base.yaml',
{
'version': 2,
'version': '2',
'services': {
'web': {
'image': 'example/web',
@ -539,7 +611,7 @@ class ConfigTest(unittest.TestCase):
override_file = config.ConfigFile(
'override.yaml',
{
'version': 2,
'version': '2',
'services': {
'web': {
'build': '/',
@ -568,7 +640,7 @@ class ConfigTest(unittest.TestCase):
base_file = config.ConfigFile(
'base.yaml',
{
'version': 2,
'version': '2',
'services': {
'web': {
'image': 'busybox:latest',
@ -584,7 +656,7 @@ class ConfigTest(unittest.TestCase):
base_file = config.ConfigFile(
'base.yaml',
{
'version': 2,
'version': '2',
'services': {
'web': {
'image': 'busybox:latest',
@ -624,8 +696,7 @@ class ConfigTest(unittest.TestCase):
assert services[0]['name'] == valid_name
def test_config_hint(self):
expected_error_msg = "(did you mean 'privileged'?)"
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
@ -636,6 +707,8 @@ class ConfigTest(unittest.TestCase):
)
)
assert "(did you mean 'privileged'?)" in excinfo.exconly()
def test_load_errors_on_uppercase_with_no_image(self):
with pytest.raises(ConfigurationError) as exc:
config.load(build_config_details({
@ -643,9 +716,41 @@ class ConfigTest(unittest.TestCase):
}, 'tests/fixtures/build-ctx'))
assert "Service 'Foo' contains uppercase characters" in exc.exconly()
def test_invalid_config_build_and_image_specified(self):
expected_error_msg = "Service 'foo' has both an image and build path specified."
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
def test_invalid_config_v1(self):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
'foo': {'image': 1},
},
'tests/fixtures/extends',
'filename.yml'
)
)
assert "foo.image contains an invalid type, it should be a string" \
in excinfo.exconly()
def test_invalid_config_v2(self):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
'version': '2',
'services': {
'foo': {'image': 1},
},
},
'tests/fixtures/extends',
'filename.yml'
)
)
assert "services.foo.image contains an invalid type, it should be a string" \
in excinfo.exconly()
def test_invalid_config_build_and_image_specified_v1(self):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
@ -656,9 +761,10 @@ class ConfigTest(unittest.TestCase):
)
)
assert "foo has both an image and build path specified." in excinfo.exconly()
def test_invalid_config_type_should_be_an_array(self):
expected_error_msg = "Service 'foo' configuration key 'links' contains an invalid type, it should be an array"
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
@ -669,10 +775,11 @@ class ConfigTest(unittest.TestCase):
)
)
assert "foo.links contains an invalid type, it should be an array" \
in excinfo.exconly()
def test_invalid_config_not_a_dictionary(self):
expected_error_msg = ("Top level object in 'filename.yml' needs to be "
"an object.")
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
['foo', 'lol'],
@ -681,9 +788,11 @@ class ConfigTest(unittest.TestCase):
)
)
assert "Top level object in 'filename.yml' needs to be an object" \
in excinfo.exconly()
def test_invalid_config_not_unique_items(self):
expected_error_msg = "has non-unique elements"
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
@ -694,10 +803,10 @@ class ConfigTest(unittest.TestCase):
)
)
assert "has non-unique elements" in excinfo.exconly()
def test_invalid_list_of_strings_format(self):
expected_error_msg = "Service 'web' configuration key 'command' contains 1"
expected_error_msg += ", which is an invalid type, it should be a string"
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
@ -708,7 +817,10 @@ class ConfigTest(unittest.TestCase):
)
)
def test_load_config_dockerfile_without_build_raises_error(self):
assert "web.command contains 1, which is an invalid type, it should be a string" \
in excinfo.exconly()
def test_load_config_dockerfile_without_build_raises_error_v1(self):
with pytest.raises(ConfigurationError) as exc:
config.load(build_config_details({
'web': {
@ -716,12 +828,11 @@ class ConfigTest(unittest.TestCase):
'dockerfile': 'Dockerfile.alt'
}
}))
assert "Service 'web' has both an image and alternate Dockerfile." in exc.exconly()
assert "web has both an image and alternate Dockerfile." in exc.exconly()
def test_config_extra_hosts_string_raises_validation_error(self):
expected_error_msg = "Service 'web' configuration key 'extra_hosts' contains an invalid type"
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{'web': {
@ -733,12 +844,11 @@ class ConfigTest(unittest.TestCase):
)
)
def test_config_extra_hosts_list_of_dicts_validation_error(self):
expected_error_msg = (
"key 'extra_hosts' contains {\"somehost\": \"162.242.195.82\"}, "
"which is an invalid type, it should be a string")
assert "web.extra_hosts contains an invalid type" \
in excinfo.exconly()
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
def test_config_extra_hosts_list_of_dicts_validation_error(self):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{'web': {
@ -753,10 +863,11 @@ class ConfigTest(unittest.TestCase):
)
)
def test_config_ulimits_invalid_keys_validation_error(self):
expected = ("Service 'web' configuration key 'ulimits' 'nofile' contains "
"unsupported option: 'not_soft_or_hard'")
assert "web.extra_hosts contains {\"somehost\": \"162.242.195.82\"}, " \
"which is an invalid type, it should be a string" \
in excinfo.exconly()
def test_config_ulimits_invalid_keys_validation_error(self):
with pytest.raises(ConfigurationError) as exc:
config.load(build_config_details(
{
@ -773,10 +884,11 @@ class ConfigTest(unittest.TestCase):
},
'working_dir',
'filename.yml'))
assert expected in exc.exconly()
assert "web.ulimits.nofile contains unsupported option: 'not_soft_or_hard'" \
in exc.exconly()
def test_config_ulimits_required_keys_validation_error(self):
with pytest.raises(ConfigurationError) as exc:
config.load(build_config_details(
{
@ -787,7 +899,7 @@ class ConfigTest(unittest.TestCase):
},
'working_dir',
'filename.yml'))
assert "Service 'web' configuration key 'ulimits' 'nofile'" in exc.exconly()
assert "web.ulimits.nofile" in exc.exconly()
assert "'hard' is a required property" in exc.exconly()
def test_config_ulimits_soft_greater_than_hard_error(self):
@ -888,7 +1000,7 @@ class ConfigTest(unittest.TestCase):
'extra_hosts': "www.example.com: 192.168.0.17",
}
}))
assert "'extra_hosts' contains an invalid type" in exc.exconly()
assert "web.extra_hosts contains an invalid type" in exc.exconly()
def test_validate_extra_hosts_invalid_list(self):
with pytest.raises(ConfigurationError) as exc:
@ -959,7 +1071,7 @@ class ConfigTest(unittest.TestCase):
def test_external_volume_config(self):
config_details = build_config_details({
'version': 2,
'version': '2',
'services': {
'bogus': {'image': 'busybox'}
},
@ -977,7 +1089,7 @@ class ConfigTest(unittest.TestCase):
def test_external_volume_invalid_config(self):
config_details = build_config_details({
'version': 2,
'version': '2',
'services': {
'bogus': {'image': 'busybox'}
},
@ -990,7 +1102,7 @@ class ConfigTest(unittest.TestCase):
def test_depends_on_orders_services(self):
config_details = build_config_details({
'version': 2,
'version': '2',
'services': {
'one': {'image': 'busybox', 'depends_on': ['three', 'two']},
'two': {'image': 'busybox', 'depends_on': ['three']},
@ -1005,7 +1117,7 @@ class ConfigTest(unittest.TestCase):
def test_depends_on_unknown_service_errors(self):
config_details = build_config_details({
'version': 2,
'version': '2',
'services': {
'one': {'image': 'busybox', 'depends_on': ['three']},
},
@ -1018,7 +1130,7 @@ class ConfigTest(unittest.TestCase):
class NetworkModeTest(unittest.TestCase):
def test_network_mode_standard(self):
config_data = config.load(build_config_details({
'version': 2,
'version': '2',
'services': {
'web': {
'image': 'busybox',
@ -1044,7 +1156,7 @@ class NetworkModeTest(unittest.TestCase):
def test_network_mode_container(self):
config_data = config.load(build_config_details({
'version': 2,
'version': '2',
'services': {
'web': {
'image': 'busybox',
@ -1069,7 +1181,7 @@ class NetworkModeTest(unittest.TestCase):
def test_network_mode_service(self):
config_data = config.load(build_config_details({
'version': 2,
'version': '2',
'services': {
'web': {
'image': 'busybox',
@ -1103,7 +1215,7 @@ class NetworkModeTest(unittest.TestCase):
def test_network_mode_service_nonexistent(self):
with pytest.raises(ConfigurationError) as excinfo:
config.load(build_config_details({
'version': 2,
'version': '2',
'services': {
'web': {
'image': 'busybox',
@ -1118,7 +1230,7 @@ class NetworkModeTest(unittest.TestCase):
def test_network_mode_plus_networks_is_invalid(self):
with pytest.raises(ConfigurationError) as excinfo:
config.load(build_config_details({
'version': 2,
'version': '2',
'services': {
'web': {
'image': 'busybox',
@ -1574,11 +1686,7 @@ class MemoryOptionsTest(unittest.TestCase):
When you set a 'memswap_limit' it is invalid config unless you also set
a mem_limit
"""
expected_error_msg = (
"Service 'foo' configuration key 'memswap_limit' is invalid: when "
"defining 'memswap_limit' you must set 'mem_limit' as well"
)
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
@ -1589,6 +1697,10 @@ class MemoryOptionsTest(unittest.TestCase):
)
)
assert "foo.memswap_limit is invalid: when defining " \
"'memswap_limit' you must set 'mem_limit' as well" \
in excinfo.exconly()
def test_validation_with_correct_memswap_values(self):
service_dict = config.load(
build_config_details(
@ -1851,7 +1963,7 @@ class ExtendsTest(unittest.TestCase):
self.assertEqual(path, expected)
def test_extends_validation_empty_dictionary(self):
with self.assertRaisesRegexp(ConfigurationError, 'service'):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
@ -1862,8 +1974,10 @@ class ExtendsTest(unittest.TestCase):
)
)
assert 'service' in excinfo.exconly()
def test_extends_validation_missing_service_key(self):
with self.assertRaisesRegexp(ConfigurationError, "'service' is a required property"):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
@ -1874,12 +1988,10 @@ class ExtendsTest(unittest.TestCase):
)
)
assert "'service' is a required property" in excinfo.exconly()
def test_extends_validation_invalid_key(self):
expected_error_msg = (
"Service 'web' configuration key 'extends' "
"contains unsupported option: 'rogue_key'"
)
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
@ -1897,12 +2009,11 @@ class ExtendsTest(unittest.TestCase):
)
)
assert "web.extends contains unsupported option: 'rogue_key'" \
in excinfo.exconly()
def test_extends_validation_sub_property_key(self):
expected_error_msg = (
"Service 'web' configuration key 'extends' 'file' contains 1, "
"which is an invalid type, it should be a string"
)
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
build_config_details(
{
@ -1919,13 +2030,16 @@ class ExtendsTest(unittest.TestCase):
)
)
assert "web.extends.file contains 1, which is an invalid type, it should be a string" \
in excinfo.exconly()
def test_extends_validation_no_file_key_no_filename_set(self):
dictionary = {'extends': {'service': 'web'}}
def load_config():
return make_service_dict('myweb', dictionary, working_dir='tests/fixtures/extends')
with pytest.raises(ConfigurationError) as excinfo:
make_service_dict('myweb', dictionary, working_dir='tests/fixtures/extends')
self.assertRaisesRegexp(ConfigurationError, 'file', load_config)
assert 'file' in excinfo.exconly()
def test_extends_validation_valid_config(self):
service = config.load(
@ -1946,7 +2060,7 @@ class ExtendsTest(unittest.TestCase):
with pytest.raises(ConfigurationError) as exc:
load_from_filename('tests/fixtures/extends/service-with-invalid-schema.yml')
assert (
"Service 'myweb' has neither an image nor a build path specified" in
"myweb has neither an image nor a build path specified" in
exc.exconly()
)
@ -1979,16 +2093,17 @@ class ExtendsTest(unittest.TestCase):
]))
def test_invalid_links_in_extended_service(self):
expected_error_msg = "services with 'links' cannot be extended"
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
with pytest.raises(ConfigurationError) as excinfo:
load_from_filename('tests/fixtures/extends/invalid-links.yml')
def test_invalid_volumes_from_in_extended_service(self):
expected_error_msg = "services with 'volumes_from' cannot be extended"
assert "services with 'links' cannot be extended" in excinfo.exconly()
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
def test_invalid_volumes_from_in_extended_service(self):
with pytest.raises(ConfigurationError) as excinfo:
load_from_filename('tests/fixtures/extends/invalid-volumes.yml')
assert "services with 'volumes_from' cannot be extended" in excinfo.exconly()
def test_invalid_net_in_extended_service(self):
with pytest.raises(ConfigurationError) as excinfo:
load_from_filename('tests/fixtures/extends/invalid-net-v2.yml')
@ -2044,10 +2159,12 @@ class ExtendsTest(unittest.TestCase):
])
def test_load_throws_error_when_base_service_does_not_exist(self):
err_msg = r'''Cannot extend service 'foo' in .*: Service not found'''
with self.assertRaisesRegexp(ConfigurationError, err_msg):
with pytest.raises(ConfigurationError) as excinfo:
load_from_filename('tests/fixtures/extends/nonexistent-service.yml')
assert "Cannot extend service 'foo'" in excinfo.exconly()
assert "Service not found" in excinfo.exconly()
def test_partial_service_config_in_extends_is_still_valid(self):
dicts = load_from_filename('tests/fixtures/extends/valid-common-config.yml')
self.assertEqual(dicts[0]['environment'], {'FOO': '1'})
@ -2140,7 +2257,7 @@ class ExtendsTest(unittest.TestCase):
tmpdir = py.test.ensuretemp('test_extends_with_mixed_version')
self.addCleanup(tmpdir.remove)
tmpdir.join('docker-compose.yml').write("""
version: 2
version: "2"
services:
web:
extends:
@ -2162,7 +2279,7 @@ class ExtendsTest(unittest.TestCase):
tmpdir = py.test.ensuretemp('test_extends_with_defined_version')
self.addCleanup(tmpdir.remove)
tmpdir.join('docker-compose.yml').write("""
version: 2
version: "2"
services:
web:
extends:
@ -2171,7 +2288,7 @@ class ExtendsTest(unittest.TestCase):
image: busybox
""")
tmpdir.join('base.yml').write("""
version: 2
version: "2"
services:
base:
volumes: ['/foo']

View File

@ -3,13 +3,13 @@ from __future__ import unicode_literals
import pytest
from compose.config.config import V1
from compose.config.config import V2_0
from compose.config.errors import ConfigurationError
from compose.config.types import parse_extra_hosts
from compose.config.types import VolumeFromSpec
from compose.config.types import VolumeSpec
from compose.const import IS_WINDOWS_PLATFORM
from tests.unit.config.config_test import V1
from tests.unit.config.config_test import V2
def test_parse_extra_hosts_list():
@ -91,26 +91,26 @@ class TestVolumesFromSpec(object):
VolumeFromSpec.parse('unknown:format:ro', self.services, V1)
def test_parse_v2_from_service(self):
volume_from = VolumeFromSpec.parse('servicea', self.services, V2)
volume_from = VolumeFromSpec.parse('servicea', self.services, V2_0)
assert volume_from == VolumeFromSpec('servicea', 'rw', 'service')
def test_parse_v2_from_service_with_mode(self):
volume_from = VolumeFromSpec.parse('servicea:ro', self.services, V2)
volume_from = VolumeFromSpec.parse('servicea:ro', self.services, V2_0)
assert volume_from == VolumeFromSpec('servicea', 'ro', 'service')
def test_parse_v2_from_container(self):
volume_from = VolumeFromSpec.parse('container:foo', self.services, V2)
volume_from = VolumeFromSpec.parse('container:foo', self.services, V2_0)
assert volume_from == VolumeFromSpec('foo', 'rw', 'container')
def test_parse_v2_from_container_with_mode(self):
volume_from = VolumeFromSpec.parse('container:foo:ro', self.services, V2)
volume_from = VolumeFromSpec.parse('container:foo:ro', self.services, V2_0)
assert volume_from == VolumeFromSpec('foo', 'ro', 'container')
def test_parse_v2_invalid_type(self):
with pytest.raises(ConfigurationError) as exc:
VolumeFromSpec.parse('bogus:foo:ro', self.services, V2)
VolumeFromSpec.parse('bogus:foo:ro', self.services, V2_0)
assert "Unknown volumes_from type 'bogus'" in exc.exconly()
def test_parse_v2_invalid(self):
with pytest.raises(ConfigurationError):
VolumeFromSpec.parse('unknown:format:ro', self.services, V2)
VolumeFromSpec.parse('unknown:format:ro', self.services, V2_0)