From 1952b5239205c82c278896ade6eb7041eb9de7eb Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 16 Feb 2016 16:48:04 -0800 Subject: [PATCH 1/2] Constraint build argument types. Numbers are cast into strings Numerical driver_opts are also valid and typecast into strings. Additional config tests. Signed-off-by: Joffrey F --- compose/config/config.py | 13 ++++++-- compose/config/fields_schema_v2.0.json | 2 +- compose/config/service_schema_v2.0.json | 15 ++++++++- compose/utils.py | 4 +++ tests/unit/config/config_test.py | 43 ++++++++++++++++++++++++- 5 files changed, 72 insertions(+), 5 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 19722b0a6..7b9e6f83d 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -16,6 +16,7 @@ from cached_property import cached_property from ..const import COMPOSEFILE_V1 as V1 from ..const import COMPOSEFILE_V2_0 as V2_0 +from ..utils import build_string_dict from .errors import CircularReference from .errors import ComposeFileNotFound from .errors import ConfigurationError @@ -292,7 +293,7 @@ def load(config_details): config_details = config_details._replace(config_files=processed_files) main_file = config_details.config_files[0] - volumes = load_mapping(config_details.config_files, 'get_volumes', 'Volume') + volumes = load_volumes(config_details.config_files) networks = load_mapping(config_details.config_files, 'get_networks', 'Network') service_dicts = load_services( config_details.working_dir, @@ -336,6 +337,14 @@ def load_mapping(config_files, get_func, entity_type): return mapping +def load_volumes(config_files): + volumes = load_mapping(config_files, 'get_volumes', 'Volume') + for volume_name, volume in volumes.items(): + if 'driver_opts' in volume: + volume['driver_opts'] = build_string_dict(volume['driver_opts']) + return volumes + + def load_services(working_dir, config_file, service_configs): def build_service(service_name, service_dict, service_names): service_config = ServiceConfig.with_abs_paths( @@ -851,7 +860,7 @@ def normalize_build(service_dict, working_dir): else: build.update(service_dict['build']) if 'args' in build: - build['args'] = resolve_build_args(build) + build['args'] = build_string_dict(resolve_build_args(build)) service_dict['build'] = build diff --git a/compose/config/fields_schema_v2.0.json b/compose/config/fields_schema_v2.0.json index 876065e51..7703adcd0 100644 --- a/compose/config/fields_schema_v2.0.json +++ b/compose/config/fields_schema_v2.0.json @@ -78,7 +78,7 @@ "driver_opts": { "type": "object", "patternProperties": { - "^.+$": {"type": "string"} + "^.+$": {"type": ["string", "number"]} } }, "external": { diff --git a/compose/config/service_schema_v2.0.json b/compose/config/service_schema_v2.0.json index f7a67818b..3196ca89e 100644 --- a/compose/config/service_schema_v2.0.json +++ b/compose/config/service_schema_v2.0.json @@ -23,7 +23,20 @@ "properties": { "context": {"type": "string"}, "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"} + "args": { + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "patternProperties": { + "^.+$": { + "type": ["string", "number"] + } + }, + "additionalProperties": false + } + ] + } }, "additionalProperties": false } diff --git a/compose/utils.py b/compose/utils.py index 29d8a695d..669df1d20 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -92,3 +92,7 @@ def json_hash(obj): def microseconds_from_time_nano(time_nano): return int(time_nano % 1000000000 / 1000) + + +def build_string_dict(source_dict): + return dict([(k, str(v)) for k, v in source_dict.items()]) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 5f7633d90..1d6f1cbb0 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -231,7 +231,7 @@ class ConfigTest(unittest.TestCase): assert volumes['simple'] == {} assert volumes['other'] == {} - def test_volume_invalid_driver_opt(self): + def test_volume_numeric_driver_opt(self): config_details = build_config_details({ 'version': '2', 'services': { @@ -241,6 +241,19 @@ class ConfigTest(unittest.TestCase): 'simple': {'driver_opts': {'size': 42}}, } }) + cfg = config.load(config_details) + assert cfg.volumes['simple']['driver_opts']['size'] == '42' + + def test_volume_invalid_driver_opt(self): + config_details = build_config_details({ + 'version': '2', + 'services': { + 'simple': {'image': 'busybox'} + }, + 'volumes': { + 'simple': {'driver_opts': {'size': True}}, + } + }) with pytest.raises(ConfigurationError) as exc: config.load(config_details) assert 'driver_opts.size contains an invalid type' in exc.exconly() @@ -608,6 +621,34 @@ class ConfigTest(unittest.TestCase): self.assertTrue('context' in service[0]['build']) self.assertEqual(service[0]['build']['dockerfile'], 'Dockerfile-alt') + def test_load_with_buildargs(self): + service = config.load( + build_config_details( + { + 'version': '2', + 'services': { + 'web': { + 'build': { + 'context': '.', + 'dockerfile': 'Dockerfile-alt', + 'args': { + 'opt1': 42, + 'opt2': 'foobar' + } + } + } + } + }, + 'tests/fixtures/extends', + 'filename.yml' + ) + ).services[0] + assert 'args' in service['build'] + assert 'opt1' in service['build']['args'] + assert isinstance(service['build']['args']['opt1'], str) + assert service['build']['args']['opt1'] == '42' + assert service['build']['args']['opt2'] == 'foobar' + def test_load_with_multiple_files_v2(self): base_file = config.ConfigFile( 'base.yaml', From 93a02e497dd19c055235936ec4cfe3d14f99d263 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 18 Feb 2016 10:53:40 -0800 Subject: [PATCH 2/2] Apply driver_opts processing to network configs Signed-off-by: Joffrey F --- compose/config/config.py | 21 +++++++++++---------- compose/utils.py | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 7b9e6f83d..dbc6b6b22 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -293,8 +293,12 @@ def load(config_details): config_details = config_details._replace(config_files=processed_files) main_file = config_details.config_files[0] - volumes = load_volumes(config_details.config_files) - networks = load_mapping(config_details.config_files, 'get_networks', 'Network') + volumes = load_mapping( + config_details.config_files, 'get_volumes', 'Volume' + ) + networks = load_mapping( + config_details.config_files, 'get_networks', 'Network' + ) service_dicts = load_services( config_details.working_dir, main_file, @@ -334,17 +338,14 @@ def load_mapping(config_files, get_func, entity_type): mapping[name] = config + if 'driver_opts' in config: + config['driver_opts'] = build_string_dict( + config['driver_opts'] + ) + return mapping -def load_volumes(config_files): - volumes = load_mapping(config_files, 'get_volumes', 'Volume') - for volume_name, volume in volumes.items(): - if 'driver_opts' in volume: - volume['driver_opts'] = build_string_dict(volume['driver_opts']) - return volumes - - def load_services(working_dir, config_file, service_configs): def build_service(service_name, service_dict, service_names): service_config = ServiceConfig.with_abs_paths( diff --git a/compose/utils.py b/compose/utils.py index 669df1d20..494beea34 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -95,4 +95,4 @@ def microseconds_from_time_nano(time_nano): def build_string_dict(source_dict): - return dict([(k, str(v)) for k, v in source_dict.items()]) + return dict((k, str(v)) for k, v in source_dict.items())