Skip to content

Commit 32af510

Browse files
committed
Move service sorting to config package.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
1 parent 16f3fc7 commit 32af510

File tree

8 files changed

+90
-90
lines changed

8 files changed

+90
-90
lines changed

compose/config/__init__.py

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from .config import ConfigurationError
33
from .config import DOCKER_CONFIG_KEYS
44
from .config import find
5-
from .config import get_service_name_from_net
65
from .config import load
76
from .config import merge_environment
87
from .config import parse_environment

compose/config/config.py

+4-13
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from .errors import ComposeFileNotFound
1414
from .errors import ConfigurationError
1515
from .interpolation import interpolate_environment_variables
16+
from .sort_services import get_service_name_from_net
17+
from .sort_services import sort_service_dicts
1618
from .types import parse_extra_hosts
1719
from .types import parse_restart_spec
1820
from .types import VolumeFromSpec
@@ -213,10 +215,10 @@ def build_service(filename, service_name, service_dict):
213215
return service_dict
214216

215217
def build_services(config_file):
216-
return [
218+
return sort_service_dicts([
217219
build_service(config_file.filename, name, service_dict)
218220
for name, service_dict in config_file.config.items()
219-
]
221+
])
220222

221223
def merge_services(base, override):
222224
all_service_names = set(base) | set(override)
@@ -645,17 +647,6 @@ def to_list(value):
645647
return value
646648

647649

648-
def get_service_name_from_net(net_config):
649-
if not net_config:
650-
return
651-
652-
if not net_config.startswith('container:'):
653-
return
654-
655-
_, net_name = net_config.split(':', 1)
656-
return net_name
657-
658-
659650
def load_yaml(filename):
660651
try:
661652
with open(filename, 'r') as fh:

compose/config/errors.py

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ def __str__(self):
66
return self.msg
77

88

9+
class DependencyError(ConfigurationError):
10+
pass
11+
12+
913
class CircularReference(ConfigurationError):
1014
def __init__(self, trail):
1115
self.trail = trail

compose/config/sort_services.py

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from compose.config.errors import DependencyError
2+
3+
4+
def get_service_name_from_net(net_config):
5+
if not net_config:
6+
return
7+
8+
if not net_config.startswith('container:'):
9+
return
10+
11+
_, net_name = net_config.split(':', 1)
12+
return net_name
13+
14+
15+
def sort_service_dicts(services):
16+
# Topological sort (Cormen/Tarjan algorithm).
17+
unmarked = services[:]
18+
temporary_marked = set()
19+
sorted_services = []
20+
21+
def get_service_names(links):
22+
return [link.split(':')[0] for link in links]
23+
24+
def get_service_names_from_volumes_from(volumes_from):
25+
return [volume_from.source for volume_from in volumes_from]
26+
27+
def get_service_dependents(service_dict, services):
28+
name = service_dict['name']
29+
return [
30+
service for service in services
31+
if (name in get_service_names(service.get('links', [])) or
32+
name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or
33+
name == get_service_name_from_net(service.get('net')))
34+
]
35+
36+
def visit(n):
37+
if n['name'] in temporary_marked:
38+
if n['name'] in get_service_names(n.get('links', [])):
39+
raise DependencyError('A service can not link to itself: %s' % n['name'])
40+
if n['name'] in n.get('volumes_from', []):
41+
raise DependencyError('A service can not mount itself as volume: %s' % n['name'])
42+
else:
43+
raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
44+
if n in unmarked:
45+
temporary_marked.add(n['name'])
46+
for m in get_service_dependents(n, services):
47+
visit(m)
48+
temporary_marked.remove(n['name'])
49+
unmarked.remove(n)
50+
sorted_services.insert(0, n)
51+
52+
while unmarked:
53+
visit(unmarked[-1])
54+
55+
return sorted_services

compose/project.py

+2-49
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from docker.errors import NotFound
99

1010
from .config import ConfigurationError
11-
from .config import get_service_name_from_net
11+
from .config.sort_services import get_service_name_from_net
1212
from .const import DEFAULT_TIMEOUT
1313
from .const import LABEL_ONE_OFF
1414
from .const import LABEL_PROJECT
@@ -26,49 +26,6 @@
2626
log = logging.getLogger(__name__)
2727

2828

29-
def sort_service_dicts(services):
30-
# Topological sort (Cormen/Tarjan algorithm).
31-
unmarked = services[:]
32-
temporary_marked = set()
33-
sorted_services = []
34-
35-
def get_service_names(links):
36-
return [link.split(':')[0] for link in links]
37-
38-
def get_service_names_from_volumes_from(volumes_from):
39-
return [volume_from.source for volume_from in volumes_from]
40-
41-
def get_service_dependents(service_dict, services):
42-
name = service_dict['name']
43-
return [
44-
service for service in services
45-
if (name in get_service_names(service.get('links', [])) or
46-
name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or
47-
name == get_service_name_from_net(service.get('net')))
48-
]
49-
50-
def visit(n):
51-
if n['name'] in temporary_marked:
52-
if n['name'] in get_service_names(n.get('links', [])):
53-
raise DependencyError('A service can not link to itself: %s' % n['name'])
54-
if n['name'] in n.get('volumes_from', []):
55-
raise DependencyError('A service can not mount itself as volume: %s' % n['name'])
56-
else:
57-
raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
58-
if n in unmarked:
59-
temporary_marked.add(n['name'])
60-
for m in get_service_dependents(n, services):
61-
visit(m)
62-
temporary_marked.remove(n['name'])
63-
unmarked.remove(n)
64-
sorted_services.insert(0, n)
65-
66-
while unmarked:
67-
visit(unmarked[-1])
68-
69-
return sorted_services
70-
71-
7229
class Project(object):
7330
"""
7431
A collection of services.
@@ -96,7 +53,7 @@ def from_dicts(cls, name, service_dicts, client, use_networking=False, network_d
9653
if use_networking:
9754
remove_links(service_dicts)
9855

99-
for service_dict in sort_service_dicts(service_dicts):
56+
for service_dict in service_dicts:
10057
links = project.get_links(service_dict)
10158
volumes_from = project.get_volumes_from(service_dict)
10259
net = project.get_net(service_dict)
@@ -424,7 +381,3 @@ def __init__(self, name):
424381

425382
def __str__(self):
426383
return self.msg
427-
428-
429-
class DependencyError(ConfigurationError):
430-
pass

tests/unit/config/config_test.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def test_load_throws_error_when_not_dict(self):
7777
)
7878
)
7979

80-
def test_config_invalid_service_names(self):
80+
def test_load_config_invalid_service_names(self):
8181
for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']:
8282
with pytest.raises(ConfigurationError) as exc:
8383
config.load(build_config_details(
@@ -232,6 +232,27 @@ def test_load_with_multiple_files_and_invalid_override(self):
232232
assert "service 'bogus' doesn't have any configuration" in exc.exconly()
233233
assert "In file 'override.yaml'" in exc.exconly()
234234

235+
def test_load_sorts_in_dependency_order(self):
236+
config_details = build_config_details({
237+
'web': {
238+
'image': 'busybox:latest',
239+
'links': ['db'],
240+
},
241+
'db': {
242+
'image': 'busybox:latest',
243+
'volumes_from': ['volume:ro']
244+
},
245+
'volume': {
246+
'image': 'busybox:latest',
247+
'volumes': ['/tmp'],
248+
}
249+
})
250+
services = config.load(config_details)
251+
252+
assert services[0]['name'] == 'volume'
253+
assert services[1]['name'] == 'db'
254+
assert services[2]['name'] == 'web'
255+
235256
def test_config_valid_service_names(self):
236257
for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']:
237258
services = config.load(

tests/unit/sort_service_test.py tests/unit/config/sort_services_test.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from .. import unittest
1+
from compose.config.errors import DependencyError
2+
from compose.config.sort_services import sort_service_dicts
23
from compose.config.types import VolumeFromSpec
3-
from compose.project import DependencyError
4-
from compose.project import sort_service_dicts
4+
from tests import unittest
55

66

77
class SortServiceTest(unittest.TestCase):

tests/unit/project_test.py

-23
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,6 @@ def test_from_dict(self):
3434
self.assertEqual(project.get_service('db').name, 'db')
3535
self.assertEqual(project.get_service('db').options['image'], 'busybox:latest')
3636

37-
def test_from_dict_sorts_in_dependency_order(self):
38-
project = Project.from_dicts('composetest', [
39-
{
40-
'name': 'web',
41-
'image': 'busybox:latest',
42-
'links': ['db'],
43-
},
44-
{
45-
'name': 'db',
46-
'image': 'busybox:latest',
47-
'volumes_from': [VolumeFromSpec('volume', 'ro')]
48-
},
49-
{
50-
'name': 'volume',
51-
'image': 'busybox:latest',
52-
'volumes': ['/tmp'],
53-
}
54-
], None)
55-
56-
self.assertEqual(project.services[0].name, 'volume')
57-
self.assertEqual(project.services[1].name, 'db')
58-
self.assertEqual(project.services[2].name, 'web')
59-
6037
def test_from_config(self):
6138
dicts = [
6239
{

0 commit comments

Comments
 (0)