diff --git a/compose/config/config_schema_v2.0.json b/compose/config/config_schema_v2.0.json index 14bafab40..2ad62ac52 100644 --- a/compose/config/config_schema_v2.0.json +++ b/compose/config/config_schema_v2.0.json @@ -41,6 +41,7 @@ } }, + "patternProperties": {"^x-": {}}, "additionalProperties": false, "definitions": { diff --git a/compose/config/config_schema_v2.1.json b/compose/config/config_schema_v2.1.json index 8a5e12834..24e6ba02c 100644 --- a/compose/config/config_schema_v2.1.json +++ b/compose/config/config_schema_v2.1.json @@ -41,6 +41,7 @@ } }, + "patternProperties": {"^x-": {}}, "additionalProperties": false, "definitions": { diff --git a/compose/config/config_schema_v2.2.json b/compose/config/config_schema_v2.2.json index 58ba409ff..86fc5df95 100644 --- a/compose/config/config_schema_v2.2.json +++ b/compose/config/config_schema_v2.2.json @@ -41,6 +41,7 @@ } }, + "patternProperties": {"^x-": {}}, "additionalProperties": false, "definitions": { diff --git a/compose/config/config_schema_v2.3.json b/compose/config/config_schema_v2.3.json index 7a9bdfdf1..a790bb405 100644 --- a/compose/config/config_schema_v2.3.json +++ b/compose/config/config_schema_v2.3.json @@ -41,6 +41,7 @@ } }, + "patternProperties": {"^x-": {}}, "additionalProperties": false, "definitions": { diff --git a/compose/config/config_schema_v3.4-beta.json b/compose/config/config_schema_v3.4-beta.json index 190c05f2c..cba063202 100644 --- a/compose/config/config_schema_v3.4-beta.json +++ b/compose/config/config_schema_v3.4-beta.json @@ -64,6 +64,7 @@ } }, + "patternProperties": {"^x-": {}}, "additionalProperties": false, "definitions": { diff --git a/compose/config/validation.py b/compose/config/validation.py index 0b7961e5a..c6722a14d 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -239,6 +239,16 @@ def handle_error_for_schema_with_id(error, path): invalid_config_key = parse_key_from_error_msg(error) return get_unsupported_config_msg(path, invalid_config_key) + if schema_id.startswith('config_schema_v'): + invalid_config_key = parse_key_from_error_msg(error) + return ('Invalid top-level property "{key}". Valid top-level ' + 'sections for this Compose file are: {properties}, and ' + 'extensions starting with "x-".\n\n{explanation}').format( + key=invalid_config_key, + properties=', '.join(error.schema['properties'].keys()), + explanation=VERSION_EXPLANATION + ) + if not error.path: return '{}\n\n{}'.format(error.message, VERSION_EXPLANATION) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 644290157..14dd01179 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -251,7 +251,7 @@ class ConfigTest(unittest.TestCase): ) ) - assert 'Additional properties are not allowed' in excinfo.exconly() + assert 'Invalid top-level property "web"' in excinfo.exconly() assert VERSION_EXPLANATION in excinfo.exconly() def test_named_volume_config_empty(self): @@ -773,6 +773,18 @@ class ConfigTest(unittest.TestCase): assert services[1]['name'] == 'db' assert services[2]['name'] == 'web' + def test_load_with_extensions(self): + config_details = build_config_details({ + 'version': '2.3', + 'x-data': { + 'lambda': 3, + 'excess': [True, {}] + } + }) + + config_data = config.load(config_details) + assert config_data.services == [] + def test_config_build_configuration(self): service = config.load( build_config_details(