浏览代码

Add type conversion (number, bool) -> float for label values

Signed-off-by: Joffrey F <[email protected]>
Joffrey F 8 年之前
父节点
当前提交
7f30a88bd6
共有 3 个文件被更改,包括 69 次插入10 次删除
  1. 24 2
      compose/config/interpolation.py
  2. 44 7
      tests/unit/config/config_test.py
  3. 1 1
      tests/unit/config/interpolation_test.py

+ 24 - 2
compose/config/interpolation.py

@@ -84,7 +84,7 @@ def recursive_interpolate(obj, interpolator, config_path):
         )
     if isinstance(obj, list):
         return [recursive_interpolate(val, interpolator, config_path) for val in obj]
-    return obj
+    return converter.convert(config_path, obj)
 
 
 class TemplateWithDefaults(Template):
@@ -160,10 +160,11 @@ class UnsetRequiredSubstitution(Exception):
 
 
 PATH_JOKER = '[^.]+'
+FULL_JOKER = '.+'
 
 
 def re_path(*args):
-    return re.compile('^{}$'.format('.'.join(args)))
+    return re.compile('^{}$'.format('\.'.join(args)))
 
 
 def re_path_basic(section, name):
@@ -175,6 +176,8 @@ def service_path(*args):
 
 
 def to_boolean(s):
+    if not isinstance(s, six.string_types):
+        return s
     s = s.lower()
     if s in ['y', 'yes', 'true', 'on']:
         return True
@@ -184,6 +187,9 @@ def to_boolean(s):
 
 
 def to_int(s):
+    if not isinstance(s, six.string_types):
+        return s
+
     # We must be able to handle octal representation for `mode` values notably
     if six.PY3 and re.match('^0[0-9]+$', s.strip()):
         s = '0o' + s[1:]
@@ -194,27 +200,39 @@ def to_int(s):
 
 
 def to_float(s):
+    if not isinstance(s, six.string_types):
+        return s
+
     try:
         return float(s)
     except ValueError:
         raise ValueError('"{}" is not a valid float'.format(s))
 
 
+def to_str(o):
+    if isinstance(o, (bool, float, int)):
+        return '{}'.format(o)
+    return o
+
+
 class ConversionMap(object):
     map = {
         service_path('blkio_config', 'weight'): to_int,
         service_path('blkio_config', 'weight_device', 'weight'): to_int,
+        service_path('build', 'labels', FULL_JOKER): to_str,
         service_path('cpus'): to_float,
         service_path('cpu_count'): to_int,
         service_path('configs', 'mode'): to_int,
         service_path('secrets', 'mode'): to_int,
         service_path('healthcheck', 'retries'): to_int,
         service_path('healthcheck', 'disable'): to_boolean,
+        service_path('deploy', 'labels', PATH_JOKER): to_str,
         service_path('deploy', 'replicas'): to_int,
         service_path('deploy', 'update_config', 'parallelism'): to_int,
         service_path('deploy', 'update_config', 'max_failure_ratio'): to_float,
         service_path('deploy', 'restart_policy', 'max_attempts'): to_int,
         service_path('mem_swappiness'): to_int,
+        service_path('labels', FULL_JOKER): to_str,
         service_path('oom_kill_disable'): to_boolean,
         service_path('oom_score_adj'): to_int,
         service_path('ports', 'target'): to_int,
@@ -232,9 +250,13 @@ class ConversionMap(object):
         re_path_basic('network', 'attachable'): to_boolean,
         re_path_basic('network', 'external'): to_boolean,
         re_path_basic('network', 'internal'): to_boolean,
+        re_path('network', PATH_JOKER, 'labels', FULL_JOKER): to_str,
         re_path_basic('volume', 'external'): to_boolean,
+        re_path('volume', PATH_JOKER, 'labels', FULL_JOKER): to_str,
         re_path_basic('secret', 'external'): to_boolean,
+        re_path('secret', PATH_JOKER, 'labels', FULL_JOKER): to_str,
         re_path_basic('config', 'external'): to_boolean,
+        re_path('config', PATH_JOKER, 'labels', FULL_JOKER): to_str,
     }
 
     def convert(self, path, value):

+ 44 - 7
tests/unit/config/config_test.py

@@ -563,7 +563,7 @@ class ConfigTest(unittest.TestCase):
                 'services': {
                     'web': {
                         'build': {
-                            'context': '.',
+                            'context': os.getcwd(),
                             'args': None,
                         },
                     },
@@ -959,7 +959,7 @@ class ConfigTest(unittest.TestCase):
         ).services[0]
         assert 'labels' in service['build']
         assert 'label1' in service['build']['labels']
-        assert service['build']['labels']['label1'] == 42
+        assert service['build']['labels']['label1'] == '42'
         assert service['build']['labels']['label2'] == 'foobar'
 
     def test_load_build_labels_list(self):
@@ -2747,24 +2747,61 @@ class ConfigTest(unittest.TestCase):
         ]
         assert service_sort(service_dicts) == service_sort(expected)
 
-    def test_config_invalid_service_label_validation(self):
+    def test_config_convertible_label_types(self):
         config_details = build_config_details(
             {
                 'version': '3.5',
                 'services': {
                     'web': {
-                        'image': 'busybox',
+                        'build': {
+                            'labels': {'testbuild': True},
+                            'context': os.getcwd()
+                        },
                         'labels': {
                             "key": 12345
                         }
                     },
                 },
+                'networks': {
+                    'foo': {
+                        'labels': {'network.ips.max': 1023}
+                    }
+                },
+                'volumes': {
+                    'foo': {
+                        'labels': {'volume.is_readonly': False}
+                    }
+                },
+                'secrets': {
+                    'foo': {
+                        'labels': {'secret.data.expires': 1546282120}
+                    }
+                },
+                'configs': {
+                    'foo': {
+                        'labels': {'config.data.correction.value': -0.1412}
+                    }
+                }
             }
         )
-        with pytest.raises(ConfigurationError) as exc:
-            config.load(config_details)
+        loaded_config = config.load(config_details)
+
+        assert loaded_config.services[0]['build']['labels'] == {'testbuild': 'True'}
+        assert loaded_config.services[0]['labels'] == {'key': '12345'}
+        assert loaded_config.networks['foo']['labels']['network.ips.max'] == '1023'
+        assert loaded_config.volumes['foo']['labels']['volume.is_readonly'] == 'False'
+        assert loaded_config.secrets['foo']['labels']['secret.data.expires'] == '1546282120'
+        assert loaded_config.configs['foo']['labels']['config.data.correction.value'] == '-0.1412'
 
-        assert "which is an invalid type, it should be a string" in exc.exconly()
+    def test_config_invalid_label_types(self):
+        config_details = build_config_details({
+            'version': '2.3',
+            'volumes': {
+                'foo': {'labels': [1, 2, 3]}
+            }
+        })
+        with pytest.raises(ConfigurationError):
+            config.load(config_details)
 
     def test_service_volume_invalid_config(self):
         config_details = build_config_details(

+ 1 - 1
tests/unit/config/interpolation_test.py

@@ -109,7 +109,7 @@ def test_interpolate_environment_variables_in_secrets(mock_env):
         'secretservice': {
             'file': 'bar',
             'labels': {
-                'max': 2,
+                'max': '2',
                 'user': 'jenny'
             }
         },