diff --git a/compose/config/config.py b/compose/config/config.py index 7abab25468..de37d8c48b 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -1061,7 +1061,12 @@ def merge_service_dicts(base, override, version): md.merge_field(field, merge_list_or_string) md.merge_field('logging', merge_logging, default={}) - merge_ports(md, base, override) + + if 'ports' in md.override.get('override', []): + md.merge_field('ports', merge_override_items_lists, default=[]) + else: + merge_ports(md, base, override) + md.merge_field('blkio_config', merge_blkio_config, default={}) md.merge_field('healthcheck', merge_healthchecks, default={}) md.merge_field('deploy', merge_deploy, default={}) @@ -1220,6 +1225,10 @@ def merge_labels(base, override): return labels +def merge_override_items_lists(base, override): + return sorted(set(override)) + + def split_kv(kvpair): if '=' in kvpair: return kvpair.split('=', 1) diff --git a/compose/config/config_schema_v2.0.json b/compose/config/config_schema_v2.0.json index 419f2e28c9..3378f6087d 100644 --- a/compose/config/config_schema_v2.0.json +++ b/compose/config/config_schema_v2.0.json @@ -256,7 +256,8 @@ "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "volume_driver": {"type": "string"}, "volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "dependencies": { diff --git a/compose/config/config_schema_v2.1.json b/compose/config/config_schema_v2.1.json index 3cb1ee2131..1fbcca5a65 100644 --- a/compose/config/config_schema_v2.1.json +++ b/compose/config/config_schema_v2.1.json @@ -289,7 +289,8 @@ "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "volume_driver": {"type": "string"}, "volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "dependencies": { diff --git a/compose/config/config_schema_v2.2.json b/compose/config/config_schema_v2.2.json index 8e1f288bad..6e3ce8dc31 100644 --- a/compose/config/config_schema_v2.2.json +++ b/compose/config/config_schema_v2.2.json @@ -298,7 +298,8 @@ "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "volume_driver": {"type": "string"}, "volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "dependencies": { diff --git a/compose/config/config_schema_v2.3.json b/compose/config/config_schema_v2.3.json index 659dbcd1a4..4658bb30a2 100644 --- a/compose/config/config_schema_v2.3.json +++ b/compose/config/config_schema_v2.3.json @@ -341,7 +341,8 @@ }, "volume_driver": {"type": "string"}, "volumes_from": {"$ref": "#/definitions/list_of_strings"}, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "dependencies": { diff --git a/compose/config/config_schema_v2.4.json b/compose/config/config_schema_v2.4.json index 4e64178871..c3245e7ca1 100644 --- a/compose/config/config_schema_v2.4.json +++ b/compose/config/config_schema_v2.4.json @@ -340,7 +340,8 @@ }, "volume_driver": {"type": "string"}, "volumes_from": {"$ref": "#/definitions/list_of_strings"}, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "dependencies": { diff --git a/compose/config/config_schema_v3.0.json b/compose/config/config_schema_v3.0.json index 10c3635215..f496ae29c2 100644 --- a/compose/config/config_schema_v3.0.json +++ b/compose/config/config_schema_v3.0.json @@ -195,7 +195,8 @@ "user": {"type": "string"}, "userns_mode": {"type": "string"}, "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "additionalProperties": false }, diff --git a/compose/config/config_schema_v3.1.json b/compose/config/config_schema_v3.1.json index 8630ec3174..986b33746d 100644 --- a/compose/config/config_schema_v3.1.json +++ b/compose/config/config_schema_v3.1.json @@ -224,7 +224,8 @@ "user": {"type": "string"}, "userns_mode": {"type": "string"}, "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "additionalProperties": false }, diff --git a/compose/config/config_schema_v3.2.json b/compose/config/config_schema_v3.2.json index 5eccdea72c..3b6ccc9e13 100644 --- a/compose/config/config_schema_v3.2.json +++ b/compose/config/config_schema_v3.2.json @@ -270,7 +270,8 @@ "uniqueItems": true } }, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "additionalProperties": false }, diff --git a/compose/config/config_schema_v3.3.json b/compose/config/config_schema_v3.3.json index f63842b9de..4d45aa1d45 100644 --- a/compose/config/config_schema_v3.3.json +++ b/compose/config/config_schema_v3.3.json @@ -303,7 +303,8 @@ "uniqueItems": true } }, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "additionalProperties": false }, diff --git a/compose/config/config_schema_v3.4.json b/compose/config/config_schema_v3.4.json index 23e9554465..e667bf5d60 100644 --- a/compose/config/config_schema_v3.4.json +++ b/compose/config/config_schema_v3.4.json @@ -307,7 +307,8 @@ "uniqueItems": true } }, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "additionalProperties": false }, diff --git a/compose/config/config_schema_v3.5.json b/compose/config/config_schema_v3.5.json index e3bdecbc1c..279e733349 100644 --- a/compose/config/config_schema_v3.5.json +++ b/compose/config/config_schema_v3.5.json @@ -308,7 +308,8 @@ "uniqueItems": true } }, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "additionalProperties": false }, diff --git a/compose/config/config_schema_v3.6.json b/compose/config/config_schema_v3.6.json index 95a552b346..0f672be928 100644 --- a/compose/config/config_schema_v3.6.json +++ b/compose/config/config_schema_v3.6.json @@ -317,7 +317,8 @@ "uniqueItems": true } }, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "additionalProperties": false }, diff --git a/compose/config/config_schema_v3.7.json b/compose/config/config_schema_v3.7.json index cd7882f5b2..5d94b90f40 100644 --- a/compose/config/config_schema_v3.7.json +++ b/compose/config/config_schema_v3.7.json @@ -318,7 +318,8 @@ "uniqueItems": true } }, - "working_dir": {"type": "string"} + "working_dir": {"type": "string"}, + "override": {"type": "array", "items": {"type": ["string"], "uniqueItems": true, "format": "override"}} }, "patternProperties": {"^x-": {}}, "additionalProperties": false diff --git a/compose/config/validation.py b/compose/config/validation.py index 0fdcb37e7d..c8c551f152 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -69,6 +69,8 @@ $ """.format(IPV6_SEG=VALID_IPV6_SEG, IPV4_ADDR=VALID_IPV4_ADDR).split()) +VALID_OVERRIDE_FORMAT = r'^(ports)$' + @FormatChecker.cls_checks(format="ports", raises=ValidationError) def format_ports(instance): @@ -99,6 +101,15 @@ def format_subnet_ip_address(instance): return True +@FormatChecker.cls_checks(format="override", raises=ValidationError) +def format_override(instance): + if isinstance(instance, six.string_types): + if not re.match(VALID_OVERRIDE_FORMAT, instance): + raise ValidationError("should be of the format 'ports'") + + return True + + def match_named_volumes(service_dict, project_volumes): service_volumes = service_dict.get('volumes', []) for volume_spec in service_volumes: @@ -426,7 +437,7 @@ def process_config_schema_errors(error): def validate_against_config_schema(config_file): schema = load_jsonschema(config_file) - format_checker = FormatChecker(["ports", "expose", "subnet_ip_address"]) + format_checker = FormatChecker(["ports", "expose", "subnet_ip_address", "override"]) validator = Draft4Validator( schema, resolver=RefResolver(get_resolver_path(), schema), diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 08b92a5731..ce86d926f0 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1205,6 +1205,58 @@ def test_volume_mode_override(self): svc_volumes = list(map(lambda v: v.repr(), service_dicts[0]['volumes'])) assert svc_volumes == ['/c:/b:ro'] + def test_config_invalid_override_config(self): + with pytest.raises(ConfigurationError) as excinfo: + config.load( + build_config_details( + { + 'version': str(V2_0), + 'services': { + 'web': { + 'image': 'example/web', + 'override': ["foo"] + } + }, + }, + filename='filename.yml', + ) + ) + assert 'services.web.override is invalid: should be of the format' in excinfo.exconly() + + def test_override_mode_ports_override(self): + base_file = config.ConfigFile( + 'base.yaml', + { + 'version': str(V2_0), + 'services': { + 'web': { + 'image': 'example/web', + 'ports': ['80:80'] + } + }, + } + ) + + override_file = config.ConfigFile( + 'override.yaml', + { + 'version': '2', + 'services': { + 'web': { + 'ports': ['8080:80'], + 'override': ['ports'], + } + } + } + ) + details = config.ConfigDetails('.', [base_file, override_file]) + service_dicts = config.load(details).services + svc_ports = list(map(lambda v: v.repr(), service_dicts[0]['ports'])) + assert svc_ports == [{ + 'published': 8080, + 'target': 80, + }] + def test_undeclared_volume_v2(self): base_file = config.ConfigFile( 'base.yaml',