Move service sorting to config package.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2015-11-17 13:35:28 -05:00
parent effa9834a5
commit 533f33271a
10 changed files with 91 additions and 92 deletions

View File

@ -2,7 +2,6 @@
from .config import ConfigurationError from .config import ConfigurationError
from .config import DOCKER_CONFIG_KEYS from .config import DOCKER_CONFIG_KEYS
from .config import find from .config import find
from .config import get_service_name_from_net
from .config import load from .config import load
from .config import merge_environment from .config import merge_environment
from .config import parse_environment from .config import parse_environment

View File

@ -14,6 +14,8 @@ from .errors import CircularReference
from .errors import ComposeFileNotFound from .errors import ComposeFileNotFound
from .errors import ConfigurationError from .errors import ConfigurationError
from .interpolation import interpolate_environment_variables from .interpolation import interpolate_environment_variables
from .sort_services import get_service_name_from_net
from .sort_services import sort_service_dicts
from .types import parse_extra_hosts from .types import parse_extra_hosts
from .types import parse_restart_spec from .types import parse_restart_spec
from .types import VolumeFromSpec from .types import VolumeFromSpec
@ -214,10 +216,10 @@ def load(config_details):
return service_dict return service_dict
def build_services(config_file): def build_services(config_file):
return [ return sort_service_dicts([
build_service(config_file.filename, name, service_dict) build_service(config_file.filename, name, service_dict)
for name, service_dict in config_file.config.items() for name, service_dict in config_file.config.items()
] ])
def merge_services(base, override): def merge_services(base, override):
all_service_names = set(base) | set(override) all_service_names = set(base) | set(override)
@ -638,17 +640,6 @@ def to_list(value):
return value return value
def get_service_name_from_net(net_config):
if not net_config:
return
if not net_config.startswith('container:'):
return
_, net_name = net_config.split(':', 1)
return net_name
def load_yaml(filename): def load_yaml(filename):
try: try:
with open(filename, 'r') as fh: with open(filename, 'r') as fh:

View File

@ -6,6 +6,10 @@ class ConfigurationError(Exception):
return self.msg return self.msg
class DependencyError(ConfigurationError):
pass
class CircularReference(ConfigurationError): class CircularReference(ConfigurationError):
def __init__(self, trail): def __init__(self, trail):
self.trail = trail self.trail = trail

View File

@ -0,0 +1,55 @@
from compose.config.errors import DependencyError
def get_service_name_from_net(net_config):
if not net_config:
return
if not net_config.startswith('container:'):
return
_, net_name = net_config.split(':', 1)
return net_name
def sort_service_dicts(services):
# Topological sort (Cormen/Tarjan algorithm).
unmarked = services[:]
temporary_marked = set()
sorted_services = []
def get_service_names(links):
return [link.split(':')[0] for link in links]
def get_service_names_from_volumes_from(volumes_from):
return [volume_from.source for volume_from in volumes_from]
def get_service_dependents(service_dict, services):
name = service_dict['name']
return [
service for service in services
if (name in get_service_names(service.get('links', [])) or
name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or
name == get_service_name_from_net(service.get('net')))
]
def visit(n):
if n['name'] in temporary_marked:
if n['name'] in get_service_names(n.get('links', [])):
raise DependencyError('A service can not link to itself: %s' % n['name'])
if n['name'] in n.get('volumes_from', []):
raise DependencyError('A service can not mount itself as volume: %s' % n['name'])
else:
raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
if n in unmarked:
temporary_marked.add(n['name'])
for m in get_service_dependents(n, services):
visit(m)
temporary_marked.remove(n['name'])
unmarked.remove(n)
sorted_services.insert(0, n)
while unmarked:
visit(unmarked[-1])
return sorted_services

View File

@ -9,7 +9,7 @@ from docker.errors import NotFound
from . import parallel from . import parallel
from .config import ConfigurationError from .config import ConfigurationError
from .config import get_service_name_from_net from .config.sort_services import get_service_name_from_net
from .const import DEFAULT_TIMEOUT from .const import DEFAULT_TIMEOUT
from .const import LABEL_ONE_OFF from .const import LABEL_ONE_OFF
from .const import LABEL_PROJECT from .const import LABEL_PROJECT
@ -26,49 +26,6 @@ from .service import ServiceNet
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def sort_service_dicts(services):
# Topological sort (Cormen/Tarjan algorithm).
unmarked = services[:]
temporary_marked = set()
sorted_services = []
def get_service_names(links):
return [link.split(':')[0] for link in links]
def get_service_names_from_volumes_from(volumes_from):
return [volume_from.source for volume_from in volumes_from]
def get_service_dependents(service_dict, services):
name = service_dict['name']
return [
service for service in services
if (name in get_service_names(service.get('links', [])) or
name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or
name == get_service_name_from_net(service.get('net')))
]
def visit(n):
if n['name'] in temporary_marked:
if n['name'] in get_service_names(n.get('links', [])):
raise DependencyError('A service can not link to itself: %s' % n['name'])
if n['name'] in n.get('volumes_from', []):
raise DependencyError('A service can not mount itself as volume: %s' % n['name'])
else:
raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
if n in unmarked:
temporary_marked.add(n['name'])
for m in get_service_dependents(n, services):
visit(m)
temporary_marked.remove(n['name'])
unmarked.remove(n)
sorted_services.insert(0, n)
while unmarked:
visit(unmarked[-1])
return sorted_services
class Project(object): class Project(object):
""" """
A collection of services. A collection of services.
@ -96,7 +53,7 @@ class Project(object):
if use_networking: if use_networking:
remove_links(service_dicts) remove_links(service_dicts)
for service_dict in sort_service_dicts(service_dicts): for service_dict in service_dicts:
links = project.get_links(service_dict) links = project.get_links(service_dict)
volumes_from = project.get_volumes_from(service_dict) volumes_from = project.get_volumes_from(service_dict)
net = project.get_net(service_dict) net = project.get_net(service_dict)
@ -404,7 +361,3 @@ class NoSuchService(Exception):
def __str__(self): def __str__(self):
return self.msg return self.msg
class DependencyError(ConfigurationError):
pass

View File

@ -8,6 +8,7 @@ from pytest import skip
from .. import unittest from .. import unittest
from compose.cli.docker_client import docker_client from compose.cli.docker_client import docker_client
from compose.config.config import resolve_environment from compose.config.config import resolve_environment
from compose.config.config import ServiceConfig
from compose.const import LABEL_PROJECT from compose.const import LABEL_PROJECT
from compose.progress_stream import stream_output from compose.progress_stream import stream_output
from compose.service import Service from compose.service import Service

View File

@ -77,7 +77,7 @@ class ConfigTest(unittest.TestCase):
) )
) )
def test_config_invalid_service_names(self): def test_load_config_invalid_service_names(self):
for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']: for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']:
with pytest.raises(ConfigurationError) as exc: with pytest.raises(ConfigurationError) as exc:
config.load(build_config_details( config.load(build_config_details(
@ -232,6 +232,27 @@ class ConfigTest(unittest.TestCase):
assert "service 'bogus' doesn't have any configuration" in exc.exconly() assert "service 'bogus' doesn't have any configuration" 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):
config_details = build_config_details({
'web': {
'image': 'busybox:latest',
'links': ['db'],
},
'db': {
'image': 'busybox:latest',
'volumes_from': ['volume:ro']
},
'volume': {
'image': 'busybox:latest',
'volumes': ['/tmp'],
}
})
services = config.load(config_details)
assert services[0]['name'] == 'volume'
assert services[1]['name'] == 'db'
assert services[2]['name'] == 'web'
def test_config_valid_service_names(self): def test_config_valid_service_names(self):
for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']: for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']:
services = config.load( services = config.load(

View File

@ -1,7 +1,7 @@
from .. import unittest from compose.config.errors import DependencyError
from compose.config.sort_services import sort_service_dicts
from compose.config.types import VolumeFromSpec from compose.config.types import VolumeFromSpec
from compose.project import DependencyError from tests import unittest
from compose.project import sort_service_dicts
class SortServiceTest(unittest.TestCase): class SortServiceTest(unittest.TestCase):

View File

@ -34,29 +34,6 @@ class ProjectTest(unittest.TestCase):
self.assertEqual(project.get_service('db').name, 'db') self.assertEqual(project.get_service('db').name, 'db')
self.assertEqual(project.get_service('db').options['image'], 'busybox:latest') self.assertEqual(project.get_service('db').options['image'], 'busybox:latest')
def test_from_dict_sorts_in_dependency_order(self):
project = Project.from_dicts('composetest', [
{
'name': 'web',
'image': 'busybox:latest',
'links': ['db'],
},
{
'name': 'db',
'image': 'busybox:latest',
'volumes_from': [VolumeFromSpec('volume', 'ro')]
},
{
'name': 'volume',
'image': 'busybox:latest',
'volumes': ['/tmp'],
}
], None)
self.assertEqual(project.services[0].name, 'volume')
self.assertEqual(project.services[1].name, 'db')
self.assertEqual(project.services[2].name, 'web')
def test_from_config(self): def test_from_config(self):
dicts = [ dicts = [
{ {

View File

@ -23,8 +23,6 @@ from compose.service import NoSuchImageError
from compose.service import parse_repository_tag from compose.service import parse_repository_tag
from compose.service import Service from compose.service import Service
from compose.service import ServiceNet from compose.service import ServiceNet
from compose.service import VolumeFromSpec
from compose.service import VolumeSpec
from compose.service import warn_on_masked_volume from compose.service import warn_on_masked_volume