Explorar o código

Implement depends_on to define an order for services in the v2 format.

Signed-off-by: Daniel Nephin <[email protected]>
Daniel Nephin %!s(int64=9) %!d(string=hai) anos
pai
achega
0bce467782

+ 12 - 3
compose/config/config.py

@@ -27,6 +27,7 @@ from .types import VolumeFromSpec
 from .types import VolumeSpec
 from .validation import validate_against_fields_schema
 from .validation import validate_against_service_schema
+from .validation import validate_depends_on
 from .validation import validate_extends_file_path
 from .validation import validate_top_level_object
 from .validation import validate_top_level_service_objects
@@ -312,9 +313,10 @@ def load_services(working_dir, config_file, service_configs):
         resolver = ServiceExtendsResolver(service_config, config_file)
         service_dict = process_service(resolver.run())
 
-        validate_service(service_dict, service_config.name, config_file.version)
+        service_config = service_config._replace(config=service_dict)
+        validate_service(service_config, service_names, config_file.version)
         service_dict = finalize_service(
-            service_config._replace(config=service_dict),
+            service_config,
             service_names,
             config_file.version)
         return service_dict
@@ -481,6 +483,10 @@ def validate_extended_service_dict(service_dict, filename, service):
             raise ConfigurationError(
                 "%s services with 'net: container' cannot be extended" % error_prefix)
 
+    if 'depends_on' in service_dict:
+        raise ConfigurationError(
+            "%s services with 'depends_on' cannot be extended" % error_prefix)
+
 
 def validate_ulimits(ulimit_config):
     for limit_name, soft_hard_values in six.iteritems(ulimit_config):
@@ -491,13 +497,16 @@ def validate_ulimits(ulimit_config):
                     "than 'hard' value".format(ulimit_config))
 
 
-def validate_service(service_dict, service_name, version):
+def validate_service(service_config, service_names, version):
+    service_dict, service_name = service_config.config, service_config.name
     validate_against_service_schema(service_dict, service_name, version)
     validate_paths(service_dict)
 
     if 'ulimits' in service_dict:
         validate_ulimits(service_dict['ulimits'])
 
+    validate_depends_on(service_config, service_names)
+
     if not service_dict.get('image') and has_uppercase(service_name):
         raise ConfigurationError(
             "Service '{name}' contains uppercase characters which are not valid "

+ 1 - 0
compose/config/service_schema_v2.json

@@ -42,6 +42,7 @@
         "cpu_shares": {"type": ["number", "string"]},
         "cpu_quota": {"type": ["number", "string"]},
         "cpuset": {"type": "string"},
+        "depends_on": {"$ref": "#/definitions/list_of_strings"},
         "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
         "dns": {"$ref": "#/definitions/string_or_list"},
         "dns_search": {"$ref": "#/definitions/string_or_list"},

+ 6 - 3
compose/config/sort_services.py

@@ -33,7 +33,8 @@ def sort_service_dicts(services):
             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')))
+                name == get_service_name_from_net(service.get('net')) or
+                name in service.get('depends_on', []))
         ]
 
     def visit(n):
@@ -42,8 +43,10 @@ def sort_service_dicts(services):
                 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['name'] in n.get('depends_on', []):
+                raise DependencyError('A service can not depend on itself: %s' % n['name'])
+            raise DependencyError('Circular dependency between %s' % ' and '.join(temporary_marked))
+
         if n in unmarked:
             temporary_marked.add(n['name'])
             for m in get_service_dependents(n, services):

+ 8 - 0
compose/config/validation.py

@@ -123,6 +123,14 @@ def validate_extends_file_path(service_name, extends_options, filename):
         )
 
 
+def validate_depends_on(service_config, service_names):
+    for dependency in service_config.config.get('depends_on', []):
+        if dependency not in service_names:
+            raise ConfigurationError(
+                "Service '{s.name}' depends on service '{dep}' which is "
+                "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)
     if error_key in DOCKER_CONFIG_HINTS:

+ 2 - 1
compose/service.py

@@ -471,7 +471,8 @@ class Service(object):
         net_name = self.net.service_name
         return (self.get_linked_service_names() +
                 self.get_volumes_from_names() +
-                ([net_name] if net_name else []))
+                ([net_name] if net_name else []) +
+                self.options.get('depends_on', []))
 
     def get_linked_service_names(self):
         return [service.name for (service, _) in self.links]

+ 27 - 1
tests/unit/config/config_test.py

@@ -894,8 +894,34 @@ class ConfigTest(unittest.TestCase):
                 'ext': {'external': True, 'driver': 'foo'}
             }
         })
-        with self.assertRaises(ConfigurationError):
+        with pytest.raises(ConfigurationError):
+            config.load(config_details)
+
+    def test_depends_on_orders_services(self):
+        config_details = build_config_details({
+            'version': 2,
+            'services': {
+                'one': {'image': 'busybox', 'depends_on': ['three', 'two']},
+                'two': {'image': 'busybox', 'depends_on': ['three']},
+                'three': {'image': 'busybox'},
+            },
+        })
+        actual = config.load(config_details)
+        assert (
+            [service['name'] for service in actual.services] ==
+            ['three', 'two', 'one']
+        )
+
+    def test_depends_on_unknown_service_errors(self):
+        config_details = build_config_details({
+            'version': 2,
+            'services': {
+                'one': {'image': 'busybox', 'depends_on': ['three']},
+            },
+        })
+        with pytest.raises(ConfigurationError) as exc:
             config.load(config_details)
+        assert "Service 'one' depends on service 'three'" in exc.exconly()
 
 
 class PortsTest(unittest.TestCase):

+ 14 - 0
tests/unit/config/sort_services_test.py

@@ -1,6 +1,8 @@
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
+import pytest
+
 from compose.config.errors import DependencyError
 from compose.config.sort_services import sort_service_dicts
 from compose.config.types import VolumeFromSpec
@@ -240,3 +242,15 @@ class SortServiceTest(unittest.TestCase):
             self.assertIn('web', e.msg)
         else:
             self.fail('Should have thrown an DependencyError')
+
+    def test_sort_service_dicts_depends_on_self(self):
+        services = [
+            {
+                'depends_on': ['web'],
+                'name': 'web'
+            },
+        ]
+
+        with pytest.raises(DependencyError) as exc:
+            sort_service_dicts(services)
+        assert 'A service can not depend on itself: web' in exc.exconly()