Merge pull request #2350 from dnephin/remove_name_from_schema

Remove name field from the config schema
This commit is contained in:
Daniel Nephin 2015-11-11 15:43:03 -05:00
commit a746d673b1
6 changed files with 74 additions and 69 deletions

View File

@ -103,6 +103,20 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
return cls(filename, load_yaml(filename))
class ServiceConfig(namedtuple('_ServiceConfig', 'working_dir filename name config')):
@classmethod
def with_abs_paths(cls, working_dir, filename, name, config):
if not working_dir:
raise ValueError("No working_dir for ServiceConfig.")
return cls(
os.path.abspath(working_dir),
os.path.abspath(filename) if filename else filename,
name,
config)
def find(base_dir, filenames):
if filenames == ['-']:
return ConfigDetails(
@ -177,19 +191,22 @@ def load(config_details):
"""
def build_service(filename, service_name, service_dict):
resolver = ServiceExtendsResolver(
service_config = ServiceConfig.with_abs_paths(
config_details.working_dir,
filename,
service_name,
service_dict)
service_dict = process_service(config_details.working_dir, resolver.run())
resolver = ServiceExtendsResolver(service_config)
service_dict = process_service(resolver.run())
validate_against_service_schema(service_dict, service_config.name)
validate_paths(service_dict)
service_dict['name'] = service_config.name
return service_dict
def build_services(filename, config):
def build_services(config_file):
return [
build_service(filename, name, service_config)
for name, service_config in config.items()
build_service(config_file.filename, name, service_dict)
for name, service_dict in config_file.config.items()
]
def merge_services(base, override):
@ -208,7 +225,7 @@ def load(config_details):
config = merge_services(config_file.config, next_file.config)
config_file = config_file._replace(config=config)
return build_services(config_file.filename, config_file.config)
return build_services(config_file)
def process_config_file(config_file, service_name=None):
@ -225,31 +242,14 @@ def process_config_file(config_file, service_name=None):
class ServiceExtendsResolver(object):
def __init__(
self,
working_dir,
filename,
service_name,
service_dict,
already_seen=None
):
if working_dir is None:
raise ValueError("No working_dir passed to ServiceExtendsResolver()")
self.working_dir = os.path.abspath(working_dir)
if filename:
self.filename = os.path.abspath(filename)
else:
self.filename = filename
def __init__(self, service_config, already_seen=None):
self.service_config = service_config
self.working_dir = service_config.working_dir
self.already_seen = already_seen or []
self.service_dict = service_dict.copy()
self.service_name = service_name
self.service_dict['name'] = service_name
@property
def signature(self):
return self.filename, self.service_name
return self.service_config.filename, self.service_config.name
def detect_cycle(self):
if self.signature in self.already_seen:
@ -258,8 +258,8 @@ class ServiceExtendsResolver(object):
def run(self):
self.detect_cycle()
service_dict = dict(self.service_dict)
env = resolve_environment(self.working_dir, self.service_dict)
service_dict = dict(self.service_config.config)
env = resolve_environment(self.working_dir, self.service_config.config)
if env:
service_dict['environment'] = env
service_dict.pop('env_file', None)
@ -267,13 +267,10 @@ class ServiceExtendsResolver(object):
if 'extends' in service_dict:
service_dict = self.resolve_extends(*self.validate_and_construct_extends())
if not self.already_seen:
validate_against_service_schema(service_dict, self.service_name)
return service_dict
return self.service_config._replace(config=service_dict)
def validate_and_construct_extends(self):
extends = self.service_dict['extends']
extends = self.service_config.config['extends']
if not isinstance(extends, dict):
extends = {'service': extends}
@ -286,33 +283,38 @@ class ServiceExtendsResolver(object):
service_config = extended_file.config[service_name]
return config_path, service_config, service_name
def resolve_extends(self, extended_config_path, service_config, service_name):
def resolve_extends(self, extended_config_path, service_dict, service_name):
resolver = ServiceExtendsResolver(
os.path.dirname(extended_config_path),
extended_config_path,
service_name,
service_config,
already_seen=self.already_seen + [self.signature],
)
ServiceConfig.with_abs_paths(
os.path.dirname(extended_config_path),
extended_config_path,
service_name,
service_dict),
already_seen=self.already_seen + [self.signature])
other_service_dict = process_service(resolver.working_dir, resolver.run())
service_config = resolver.run()
other_service_dict = process_service(service_config)
validate_extended_service_dict(
other_service_dict,
extended_config_path,
service_name,
)
return merge_service_dicts(other_service_dict, self.service_dict)
return merge_service_dicts(other_service_dict, self.service_config.config)
def get_extended_config_path(self, extends_options):
"""Service we are extending either has a value for 'file' set, which we
need to obtain a full path too or we are extending from a service
defined in our own file.
"""
validate_extends_file_path(self.service_name, extends_options, self.filename)
filename = self.service_config.filename
validate_extends_file_path(
self.service_config.name,
extends_options,
filename)
if 'file' in extends_options:
return expand_path(self.working_dir, extends_options['file'])
return self.filename
return filename
def resolve_environment(working_dir, service_dict):
@ -357,8 +359,9 @@ def validate_ulimits(ulimit_config):
"than 'hard' value".format(ulimit_config))
def process_service(working_dir, service_dict):
service_dict = dict(service_dict)
def process_service(service_config):
working_dir = service_config.working_dir
service_dict = dict(service_config.config)
if 'volumes' in service_dict and service_dict.get('volume_driver') is None:
service_dict['volumes'] = resolve_volume_paths(working_dir, service_dict)
@ -505,12 +508,12 @@ def env_vars_from_file(filename):
def resolve_volume_paths(working_dir, service_dict):
return [
resolve_volume_path(working_dir, volume, service_dict['name'])
resolve_volume_path(working_dir, volume)
for volume in service_dict['volumes']
]
def resolve_volume_path(working_dir, volume, service_name):
def resolve_volume_path(working_dir, volume):
container_path, host_path = split_path_mapping(volume)
if host_path is not None:

View File

@ -89,7 +89,6 @@
"mac_address": {"type": "string"},
"mem_limit": {"type": ["number", "string"]},
"memswap_limit": {"type": ["number", "string"]},
"name": {"type": "string"},
"net": {"type": "string"},
"pid": {"type": ["string", "null"]},

View File

@ -3,12 +3,6 @@
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"],
"allOf": [
{"$ref": "fields_schema.json#/definitions/service"},
{"$ref": "#/definitions/service_constraints"}

View File

@ -195,7 +195,7 @@ def process_errors(errors, service_name=None):
for error in errors:
# handle root level errors
if len(error.path) == 0 and not error.instance.get('name'):
if len(error.path) == 0 and not service_name:
if error.validator == 'type':
msg = "Top level object needs to be a dictionary. Check your .yml file that you have defined a service at the top level."
root_msgs.append(msg)

View File

@ -9,6 +9,7 @@ from .. import unittest
from compose.cli.docker_client import docker_client
from compose.config.config import process_service
from compose.config.config import resolve_environment
from compose.config.config import ServiceConfig
from compose.const import LABEL_PROJECT
from compose.progress_stream import stream_output
from compose.service import Service
@ -43,15 +44,13 @@ class DockerClientTestCase(unittest.TestCase):
if 'command' not in kwargs:
kwargs['command'] = ["top"]
# TODO: remove this once #2299 is fixed
kwargs['name'] = name
options = process_service('.', kwargs)
service_config = ServiceConfig('.', None, name, kwargs)
options = process_service(service_config)
options['environment'] = resolve_environment('.', kwargs)
labels = options.setdefault('labels', {})
labels['com.docker.compose.test-name'] = self.id()
return Service(client=self.client, project='composetest', **options)
return Service(name, client=self.client, project='composetest', **options)
def check_build(self, *args, **kwargs):
kwargs.setdefault('rm', True)

View File

@ -20,12 +20,12 @@ def make_service_dict(name, service_dict, working_dir, filename=None):
"""
Test helper function to construct a ServiceExtendsResolver
"""
resolver = config.ServiceExtendsResolver(
resolver = config.ServiceExtendsResolver(config.ServiceConfig(
working_dir=working_dir,
filename=filename,
service_name=name,
service_dict=service_dict)
return config.process_service(working_dir, resolver.run())
name=name,
config=service_dict))
return config.process_service(resolver.run())
def service_sort(services):
@ -77,8 +77,8 @@ class ConfigTest(unittest.TestCase):
)
def test_config_invalid_service_names(self):
with self.assertRaises(ConfigurationError):
for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']:
for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']:
with pytest.raises(ConfigurationError):
config.load(
build_config_details(
{invalid_name: {'image': 'busybox'}},
@ -87,6 +87,16 @@ class ConfigTest(unittest.TestCase):
)
)
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()
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):
@ -651,7 +661,7 @@ class VolumeConfigTest(unittest.TestCase):
def test_volume_path_with_non_ascii_directory(self):
volume = u'/Füü/data:/data'
container_path = config.resolve_volume_path(".", volume, "test")
container_path = config.resolve_volume_path(".", volume)
self.assertEqual(container_path, volume)