Преглед изворни кода

Merge pull request #2019 from mrfuxi/less-verbose-extends

Allow to extend service using shorthand notation. Closes #1989
mnowster пре 10 година
родитељ
комит
6b68a84b9b

+ 7 - 3
compose/config/config.py

@@ -269,15 +269,19 @@ class ServiceLoader(object):
         self.service_dict['environment'] = env
 
     def validate_and_construct_extends(self):
+        extends = self.service_dict['extends']
+        if not isinstance(extends, dict):
+            extends = {'service': extends}
+
         validate_extends_file_path(
             self.service_name,
-            self.service_dict['extends'],
+            extends,
             self.filename
         )
         self.extended_config_path = self.get_extended_config_path(
-            self.service_dict['extends']
+            extends
         )
-        self.extended_service_name = self.service_dict['extends']['service']
+        self.extended_service_name = extends['service']
 
         full_extended_config = pre_process_config(
             load_yaml(self.extended_config_path)

+ 14 - 7
compose/config/fields_schema.json

@@ -58,14 +58,21 @@
         },
 
         "extends": {
-          "type": "object",
+          "oneOf": [
+            {
+              "type": "string"
+            },
+            {
+              "type": "object",
 
-          "properties": {
-            "service": {"type": "string"},
-            "file": {"type": "string"}
-          },
-          "required": ["service"],
-          "additionalProperties": false
+              "properties": {
+                "service": {"type": "string"},
+                "file": {"type": "string"}
+              },
+              "required": ["service"],
+              "additionalProperties": false
+            }
+          ]
         },
 
         "extra_hosts": {"$ref": "#/definitions/list_or_dict"},

+ 17 - 1
compose/config/validation.py

@@ -4,6 +4,7 @@ import os
 import sys
 from functools import wraps
 
+import six
 from docker.utils.ports import split_port
 from jsonschema import Draft4Validator
 from jsonschema import FormatChecker
@@ -162,10 +163,25 @@ def process_errors(errors, service_name=None):
         Inspecting the context value of a ValidationError gives us information about
         which sub schema failed and which kind of error it is.
         """
+
+        required = [context for context in error.context if context.validator == 'required']
+        if required:
+            return required[0].message
+
+        additionalProperties = [context for context in error.context if context.validator == 'additionalProperties']
+        if additionalProperties:
+            invalid_config_key = _parse_key_from_error_msg(additionalProperties[0])
+            return "contains unsupported option: '{}'".format(invalid_config_key)
+
         constraint = [context for context in error.context if len(context.path) > 0]
         if constraint:
             valid_types = _parse_valid_types_from_validator(constraint[0].validator_value)
-            msg = "contains {}, which is an invalid type, it should be {}".format(
+            invalid_config_key = "".join(
+                "'{}' ".format(fragment) for fragment in constraint[0].path
+                if isinstance(fragment, six.string_types)
+            )
+            msg = "{}contains {}, which is an invalid type, it should be {}".format(
+                invalid_config_key,
                 constraint[0].instance,
                 valid_types
             )

+ 15 - 0
tests/fixtures/extends/verbose-and-shorthand.yml

@@ -0,0 +1,15 @@
+base:
+  image: busybox
+  environment:
+    - "BAR=1"
+
+verbose:
+  extends:
+    service: base
+  environment:
+    - "FOO=1"
+
+shorthand:
+  extends: base
+  environment:
+    - "FOO=2"

+ 29 - 3
tests/unit/config/config_test.py

@@ -335,7 +335,7 @@ class ConfigTest(unittest.TestCase):
         self.assertTrue(expected_warning_msg in mock_logging.warn.call_args[0][0])
 
     def test_config_invalid_environment_dict_key_raises_validation_error(self):
-        expected_error_msg = "Service 'web' configuration key 'environment' contains an invalid type"
+        expected_error_msg = "Service 'web' configuration key 'environment' contains unsupported option: '---'"
 
         with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
             config.load(
@@ -942,7 +942,10 @@ class ExtendsTest(unittest.TestCase):
             )
 
     def test_extends_validation_invalid_key(self):
-        expected_error_msg = "Unsupported config option for 'web' service: 'rogue_key'"
+        expected_error_msg = (
+            "Service 'web' configuration key 'extends' "
+            "contains unsupported option: 'rogue_key'"
+        )
         with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
             config.load(
                 build_config_details(
@@ -962,7 +965,10 @@ class ExtendsTest(unittest.TestCase):
             )
 
     def test_extends_validation_sub_property_key(self):
-        expected_error_msg = "Service 'web' configuration key 'extends' 'file' contains an invalid type"
+        expected_error_msg = (
+            "Service 'web' configuration key 'extends' 'file' contains 1, "
+            "which is an invalid type, it should be a string"
+        )
         with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
             config.load(
                 build_config_details(
@@ -1100,6 +1106,26 @@ class ExtendsTest(unittest.TestCase):
         dicts = load_from_filename('tests/fixtures/extends/valid-common-config.yml')
         self.assertEqual(dicts[0]['environment'], {'FOO': '1'})
 
+    def test_extended_service_with_verbose_and_shorthand_way(self):
+        services = load_from_filename('tests/fixtures/extends/verbose-and-shorthand.yml')
+        self.assertEqual(service_sort(services), service_sort([
+            {
+                'name': 'base',
+                'image': 'busybox',
+                'environment': {'BAR': '1'},
+            },
+            {
+                'name': 'verbose',
+                'image': 'busybox',
+                'environment': {'BAR': '1', 'FOO': '1'},
+            },
+            {
+                'name': 'shorthand',
+                'image': 'busybox',
+                'environment': {'BAR': '1', 'FOO': '2'},
+            },
+        ]))
+
 
 @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
 class ExpandPathTest(unittest.TestCase):