浏览代码

Merge pull request #2785 from dnephin/depends_on_with_extends

Fix list options when used with extends and multiple files
Daniel Nephin 10 年之前
父节点
当前提交
4c4e94bc19
共有 4 个文件被更改,包括 122 次插入44 次删除
  1. 65 27
      compose/config/config.py
  2. 19 0
      compose/config/types.py
  3. 10 17
      docs/extends.md
  4. 28 0
      tests/unit/config/config_test.py

+ 65 - 27
compose/config/config.py

@@ -26,6 +26,7 @@ from .sort_services import get_service_name_from_network_mode
 from .sort_services import sort_service_dicts
 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 ServiceLink
 from .types import VolumeFromSpec
 from .types import VolumeFromSpec
 from .types import VolumeSpec
 from .types import VolumeSpec
 from .validation import match_named_volumes
 from .validation import match_named_volumes
@@ -85,8 +86,6 @@ ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [
     'build',
     'build',
     'container_name',
     'container_name',
     'dockerfile',
     'dockerfile',
-    'expose',
-    'external_links',
     'logging',
     'logging',
 ]
 ]
 
 
@@ -643,44 +642,79 @@ def merge_service_dicts_from_files(base, override, version):
     return new_service
     return new_service
 
 
 
 
-def merge_service_dicts(base, override, version):
-    d = {}
+class MergeDict(dict):
+    """A dict-like object responsible for merging two dicts into one."""
+
+    def __init__(self, base, override):
+        self.base = base
+        self.override = override
+
+    def needs_merge(self, field):
+        return field in self.base or field in self.override
+
+    def merge_field(self, field, merge_func, default=None):
+        if not self.needs_merge(field):
+            return
+
+        self[field] = merge_func(
+            self.base.get(field, default),
+            self.override.get(field, default))
+
+    def merge_mapping(self, field, parse_func):
+        if not self.needs_merge(field):
+            return
+
+        self[field] = parse_func(self.base.get(field))
+        self[field].update(parse_func(self.override.get(field)))
 
 
-    def merge_field(field, merge_func, default=None):
-        if field in base or field in override:
-            d[field] = merge_func(
-                base.get(field, default),
-                override.get(field, default))
+    def merge_sequence(self, field, parse_func):
+        def parse_sequence_func(seq):
+            return to_mapping((parse_func(item) for item in seq), 'merge_field')
 
 
-    def merge_mapping(mapping, parse_func):
-        if mapping in base or mapping in override:
-            merged = parse_func(base.get(mapping, None))
-            merged.update(parse_func(override.get(mapping, None)))
-            d[mapping] = merged
+        if not self.needs_merge(field):
+            return
 
 
-    merge_mapping('environment', parse_environment)
-    merge_mapping('labels', parse_labels)
-    merge_mapping('ulimits', parse_ulimits)
+        merged = parse_sequence_func(self.base.get(field, []))
+        merged.update(parse_sequence_func(self.override.get(field, [])))
+        self[field] = [item.repr() for item in merged.values()]
+
+    def merge_scalar(self, field):
+        if self.needs_merge(field):
+            self[field] = self.override.get(field, self.base.get(field))
+
+
+def merge_service_dicts(base, override, version):
+    md = MergeDict(base, override)
+
+    md.merge_mapping('environment', parse_environment)
+    md.merge_mapping('labels', parse_labels)
+    md.merge_mapping('ulimits', parse_ulimits)
+    md.merge_sequence('links', ServiceLink.parse)
 
 
     for field in ['volumes', 'devices']:
     for field in ['volumes', 'devices']:
-        merge_field(field, merge_path_mappings)
+        md.merge_field(field, merge_path_mappings)
 
 
-    for field in ['ports', 'expose', 'external_links']:
-        merge_field(field, operator.add, default=[])
+    for field in [
+        'depends_on',
+        'expose',
+        'external_links',
+        'ports',
+        'volumes_from',
+    ]:
+        md.merge_field(field, operator.add, default=[])
 
 
     for field in ['dns', 'dns_search', 'env_file']:
     for field in ['dns', 'dns_search', 'env_file']:
-        merge_field(field, merge_list_or_string)
+        md.merge_field(field, merge_list_or_string)
 
 
-    for field in set(ALLOWED_KEYS) - set(d):
-        if field in base or field in override:
-            d[field] = override.get(field, base.get(field))
+    for field in set(ALLOWED_KEYS) - set(md):
+        md.merge_scalar(field)
 
 
     if version == V1:
     if version == V1:
-        legacy_v1_merge_image_or_build(d, base, override)
+        legacy_v1_merge_image_or_build(md, base, override)
     else:
     else:
-        merge_build(d, base, override)
+        merge_build(md, base, override)
 
 
-    return d
+    return dict(md)
 
 
 
 
 def merge_build(output, base, override):
 def merge_build(output, base, override):
@@ -914,6 +948,10 @@ def to_list(value):
         return value
         return value
 
 
 
 
+def to_mapping(sequence, key_field):
+    return {getattr(item, key_field): item for item in sequence}
+
+
 def has_uppercase(name):
 def has_uppercase(name):
     return any(char in string.ascii_uppercase for char in name)
     return any(char in string.ascii_uppercase for char in name)
 
 

+ 19 - 0
compose/config/types.py

@@ -168,3 +168,22 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
     @property
     @property
     def is_named_volume(self):
     def is_named_volume(self):
         return self.external and not self.external.startswith(('.', '/', '~'))
         return self.external and not self.external.startswith(('.', '/', '~'))
+
+
+class ServiceLink(namedtuple('_ServiceLink', 'target alias')):
+
+    @classmethod
+    def parse(cls, link_spec):
+        target, _, alias = link_spec.partition(':')
+        if not alias:
+            alias = target
+        return cls(target, alias)
+
+    def repr(self):
+        if self.target == self.alias:
+            return self.target
+        return '{s.target}:{s.alias}'.format(s=self)
+
+    @property
+    def merge_field(self):
+        return self.alias

+ 10 - 17
docs/extends.md

@@ -32,12 +32,9 @@ contains your base configuration. The override file, as its name implies, can
 contain configuration overrides for existing services or entirely new
 contain configuration overrides for existing services or entirely new
 services.
 services.
 
 
-If a service is defined in both files, Compose merges the configurations using
-the same rules as the `extends` field (see [Adding and overriding
-configuration](#adding-and-overriding-configuration)), with one exception.  If a
-service contains `links` or `volumes_from` those fields are copied over and
-replace any values in the original service, in the same way single-valued fields
-are copied.
+If a service is defined in both files Compose merges the configurations using
+the rules described in [Adding and overriding
+configuration](#adding-and-overriding-configuration).
 
 
 To use multiple override files, or an override file with a different name, you
 To use multiple override files, or an override file with a different name, you
 can use the `-f` option to specify the list of files. Compose merges files in
 can use the `-f` option to specify the list of files. Compose merges files in
@@ -176,10 +173,12 @@ is useful if you have several services that reuse a common set of configuration
 options. Using `extends` you can define a common set of service options in one
 options. Using `extends` you can define a common set of service options in one
 place and refer to it from anywhere.
 place and refer to it from anywhere.
 
 
-> **Note:** `links` and `volumes_from` are never shared between services using
-> `extends`. See
-> [Adding and overriding configuration](#adding-and-overriding-configuration)
- > for more information.
+> **Note:** `links`, `volumes_from`, and `depends_on` are never shared between
+> services using >`extends`. These exceptions exist to avoid
+> implicit dependencies—you always define `links` and `volumes_from`
+> locally. This ensures dependencies between services are clearly visible when
+> reading the current file. Defining these locally also ensures changes to the
+> referenced file don't result in breakage.
 
 
 ### Understand the extends configuration
 ### Understand the extends configuration
 
 
@@ -275,13 +274,7 @@ common configuration:
 
 
 ## Adding and overriding configuration
 ## Adding and overriding configuration
 
 
-Compose copies configurations from the original service over to the local one,
-**except** for `links` and `volumes_from`. These exceptions exist to avoid
-implicit dependencies—you always define `links` and `volumes_from`
-locally. This ensures dependencies between services are clearly visible when
-reading the current file. Defining these locally also ensures changes to the
-referenced file don't result in breakage.
-
+Compose copies configurations from the original service over to the local one.
 If a configuration option is defined in both the original service the local
 If a configuration option is defined in both the original service the local
 service, the local value *replaces* or *extends* the original value.
 service, the local value *replaces* or *extends* the original value.
 
 

+ 28 - 0
tests/unit/config/config_test.py

@@ -602,6 +602,7 @@ class ConfigTest(unittest.TestCase):
                 'services': {
                 'services': {
                     'web': {
                     'web': {
                         'image': 'example/web',
                         'image': 'example/web',
+                        'depends_on': ['db'],
                     },
                     },
                     'db': {
                     'db': {
                         'image': 'example/db',
                         'image': 'example/db',
@@ -616,7 +617,11 @@ class ConfigTest(unittest.TestCase):
                     'web': {
                     'web': {
                         'build': '/',
                         'build': '/',
                         'volumes': ['/home/user/project:/code'],
                         'volumes': ['/home/user/project:/code'],
+                        'depends_on': ['other'],
                     },
                     },
+                    'other': {
+                        'image': 'example/other',
+                    }
                 }
                 }
             })
             })
         details = config.ConfigDetails('.', [base_file, override_file])
         details = config.ConfigDetails('.', [base_file, override_file])
@@ -628,11 +633,16 @@ class ConfigTest(unittest.TestCase):
                 'build': {'context': os.path.abspath('/')},
                 'build': {'context': os.path.abspath('/')},
                 'image': 'example/web',
                 'image': 'example/web',
                 'volumes': [VolumeSpec.parse('/home/user/project:/code')],
                 'volumes': [VolumeSpec.parse('/home/user/project:/code')],
+                'depends_on': ['db', 'other'],
             },
             },
             {
             {
                 'name': 'db',
                 'name': 'db',
                 'image': 'example/db',
                 'image': 'example/db',
             },
             },
+            {
+                'name': 'other',
+                'image': 'example/other',
+            },
         ]
         ]
         assert service_sort(service_dicts) == service_sort(expected)
         assert service_sort(service_dicts) == service_sort(expected)
 
 
@@ -2299,6 +2309,24 @@ class ExtendsTest(unittest.TestCase):
         service = load_from_filename(str(tmpdir.join('docker-compose.yml')))
         service = load_from_filename(str(tmpdir.join('docker-compose.yml')))
         self.assertEquals(service[0]['command'], "top")
         self.assertEquals(service[0]['command'], "top")
 
 
+    def test_extends_with_depends_on(self):
+        tmpdir = py.test.ensuretemp('test_extends_with_defined_version')
+        self.addCleanup(tmpdir.remove)
+        tmpdir.join('docker-compose.yml').write("""
+            version: "2"
+            services:
+              base:
+                image: example
+              web:
+                extends: base
+                image: busybox
+                depends_on: ['other']
+              other:
+                image: example
+        """)
+        services = load_from_filename(str(tmpdir.join('docker-compose.yml')))
+        assert service_sort(services)[2]['depends_on'] == ['other']
+
 
 
 @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
 @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
 class ExpandPathTest(unittest.TestCase):
 class ExpandPathTest(unittest.TestCase):