From d906f9ce92fe66ad89d4d0a95480e13f0aac9215 Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Thu, 13 Apr 2017 21:51:41 +1000 Subject: [PATCH 1/5] Add support for labels during build Signed-off-by: Colin Hebert --- compose/config/config_schema_v3.2.json | 1 + compose/service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/compose/config/config_schema_v3.2.json b/compose/config/config_schema_v3.2.json index ea702fcd581..70ff6ce0564 100644 --- a/compose/config/config_schema_v3.2.json +++ b/compose/config/config_schema_v3.2.json @@ -72,6 +72,7 @@ "context": {"type": "string"}, "dockerfile": {"type": "string"}, "args": {"$ref": "#/definitions/list_or_dict"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, "cache_from": {"$ref": "#/definitions/list_of_strings"} }, "additionalProperties": false diff --git a/compose/service.py b/compose/service.py index 19873d5e5cd..dcbbe251ed0 100644 --- a/compose/service.py +++ b/compose/service.py @@ -884,6 +884,7 @@ def build(self, no_cache=False, pull=False, force_rm=False, build_args_override= nocache=no_cache, dockerfile=build_opts.get('dockerfile', None), cache_from=build_opts.get('cache_from', None), + labels=build_opts.get('labels', None), buildargs=build_args ) From 7fca689efd90427f546cce18e3bbc022ffde412b Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Thu, 13 Apr 2017 22:21:33 +1000 Subject: [PATCH 2/5] Update tests to show labels set to None Signed-off-by: Colin Hebert --- tests/unit/service_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index c32c3633943..7b7a078f8cd 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -471,6 +471,7 @@ def test_create_container(self): nocache=False, rm=True, buildargs={}, + labels=None, cache_from=None, ) @@ -508,6 +509,7 @@ def test_ensure_image_exists_force_build(self): nocache=False, rm=True, buildargs={}, + labels=None, cache_from=None, ) From 37de55865b22852ce4bf29b2218dc8d1f2fc8824 Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Thu, 13 Apr 2017 22:35:21 +1000 Subject: [PATCH 3/5] Add tests for the labels Signed-off-by: Colin Hebert --- tests/unit/config/config_test.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index e66e952f83f..3d42b8392e2 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -825,6 +825,34 @@ def test_load_with_buildargs(self): assert service['build']['args']['opt1'] == '42' assert service['build']['args']['opt2'] == 'foobar' + def test_load_with_labels(self): + service = config.load( + build_config_details( + { + 'version': '3.2', + 'services': { + 'web': { + 'build': { + 'context': '.', + 'dockerfile': 'Dockerfile-alt', + 'labels': { + 'label1': 42, + 'label2': 'foobar' + } + } + } + } + }, + 'tests/fixtures/extends', + 'filename.yml' + ) + ).services[0] + assert 'labels' in service['build'] + assert 'label1' in service['build']['labels'] + assert isinstance(service['build']['labels']['label1'], str) + assert service['build']['labels']['label1'] == '42' + assert service['build']['labels']['label2'] == 'foobar' + def test_build_args_allow_empty_properties(self): service = config.load( build_config_details( From 9d78258b444f9038d80b0543e93fa5424ba4f0d7 Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Thu, 13 Apr 2017 22:40:07 +1000 Subject: [PATCH 4/5] Fix test type Signed-off-by: Colin Hebert --- tests/unit/config/config_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 3d42b8392e2..bc51600357b 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -849,8 +849,7 @@ def test_load_with_labels(self): ).services[0] assert 'labels' in service['build'] assert 'label1' in service['build']['labels'] - assert isinstance(service['build']['labels']['label1'], str) - assert service['build']['labels']['label1'] == '42' + assert service['build']['labels']['label1'] == 42 assert service['build']['labels']['label2'] == 'foobar' def test_build_args_allow_empty_properties(self): From fa77856c8641c076d8d3652bbe354a7cebfab172 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 23 May 2017 11:47:59 -0700 Subject: [PATCH 5/5] Add 3.3 format support Remove build.labels field from 3.2 schema Signed-off-by: Joffrey F --- compose/config/config_schema_v3.2.json | 1 - compose/config/config_schema_v3.3.json | 534 +++++++++++++++++++++++++ compose/config/serialize.py | 5 +- compose/const.py | 3 + docker-compose.spec | 5 + tests/unit/config/config_test.py | 5 +- 6 files changed, 548 insertions(+), 5 deletions(-) create mode 100644 compose/config/config_schema_v3.3.json diff --git a/compose/config/config_schema_v3.2.json b/compose/config/config_schema_v3.2.json index 70ff6ce0564..ea702fcd581 100644 --- a/compose/config/config_schema_v3.2.json +++ b/compose/config/config_schema_v3.2.json @@ -72,7 +72,6 @@ "context": {"type": "string"}, "dockerfile": {"type": "string"}, "args": {"$ref": "#/definitions/list_or_dict"}, - "labels": {"$ref": "#/definitions/list_or_dict"}, "cache_from": {"$ref": "#/definitions/list_of_strings"} }, "additionalProperties": false diff --git a/compose/config/config_schema_v3.3.json b/compose/config/config_schema_v3.3.json new file mode 100644 index 00000000000..e69116c3889 --- /dev/null +++ b/compose/config/config_schema_v3.3.json @@ -0,0 +1,534 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "config_schema_v3.3.json", + "type": "object", + "required": ["version"], + + "properties": { + "version": { + "type": "string" + }, + + "services": { + "id": "#/properties/services", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/service" + } + }, + "additionalProperties": false + }, + + "networks": { + "id": "#/properties/networks", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/network" + } + } + }, + + "volumes": { + "id": "#/properties/volumes", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/volume" + } + }, + "additionalProperties": false + }, + + "secrets": { + "id": "#/properties/secrets", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/secret" + } + }, + "additionalProperties": false + }, + + "configs": { + "id": "#/properties/configs", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/config" + } + }, + "additionalProperties": false + } + }, + + "additionalProperties": false, + + "definitions": { + + "service": { + "id": "#/definitions/service", + "type": "object", + + "properties": { + "deploy": {"$ref": "#/definitions/deployment"}, + "build": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "context": {"type": "string"}, + "dockerfile": {"type": "string"}, + "args": {"$ref": "#/definitions/list_or_dict"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "cache_from": {"$ref": "#/definitions/list_of_strings"} + }, + "additionalProperties": false + } + ] + }, + "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cgroup_parent": {"type": "string"}, + "command": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "configs": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "source": {"type": "string"}, + "target": {"type": "string"}, + "uid": {"type": "string"}, + "gid": {"type": "string"}, + "mode": {"type": "number"} + } + } + ] + } + }, + "container_name": {"type": "string"}, + "credential_spec": {"type": "object", "properties": { + "file": {"type": "string"}, + "registry": {"type": "string"} + }}, + "depends_on": {"$ref": "#/definitions/list_of_strings"}, + "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "dns": {"$ref": "#/definitions/string_or_list"}, + "dns_search": {"$ref": "#/definitions/string_or_list"}, + "domainname": {"type": "string"}, + "entrypoint": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "env_file": {"$ref": "#/definitions/string_or_list"}, + "environment": {"$ref": "#/definitions/list_or_dict"}, + + "expose": { + "type": "array", + "items": { + "type": ["string", "number"], + "format": "expose" + }, + "uniqueItems": true + }, + + "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, + "healthcheck": {"$ref": "#/definitions/healthcheck"}, + "hostname": {"type": "string"}, + "image": {"type": "string"}, + "ipc": {"type": "string"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + + "logging": { + "type": "object", + + "properties": { + "driver": {"type": "string"}, + "options": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number", "null"]} + } + } + }, + "additionalProperties": false + }, + + "mac_address": {"type": "string"}, + "network_mode": {"type": "string"}, + + "networks": { + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "oneOf": [ + { + "type": "object", + "properties": { + "aliases": {"$ref": "#/definitions/list_of_strings"}, + "ipv4_address": {"type": "string"}, + "ipv6_address": {"type": "string"} + }, + "additionalProperties": false + }, + {"type": "null"} + ] + } + }, + "additionalProperties": false + } + ] + }, + "pid": {"type": ["string", "null"]}, + + "ports": { + "type": "array", + "items": { + "oneOf": [ + {"type": "number", "format": "ports"}, + {"type": "string", "format": "ports"}, + { + "type": "object", + "properties": { + "mode": {"type": "string"}, + "target": {"type": "integer"}, + "published": {"type": "integer"}, + "protocol": {"type": "string"} + }, + "additionalProperties": false + } + ] + }, + "uniqueItems": true + }, + + "privileged": {"type": "boolean"}, + "read_only": {"type": "boolean"}, + "restart": {"type": "string"}, + "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "shm_size": {"type": ["number", "string"]}, + "secrets": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "source": {"type": "string"}, + "target": {"type": "string"}, + "uid": {"type": "string"}, + "gid": {"type": "string"}, + "mode": {"type": "number"} + } + } + ] + } + }, + "sysctls": {"$ref": "#/definitions/list_or_dict"}, + "stdin_open": {"type": "boolean"}, + "stop_grace_period": {"type": "string", "format": "duration"}, + "stop_signal": {"type": "string"}, + "tmpfs": {"$ref": "#/definitions/string_or_list"}, + "tty": {"type": "boolean"}, + "ulimits": { + "type": "object", + "patternProperties": { + "^[a-z]+$": { + "oneOf": [ + {"type": "integer"}, + { + "type":"object", + "properties": { + "hard": {"type": "integer"}, + "soft": {"type": "integer"} + }, + "required": ["soft", "hard"], + "additionalProperties": false + } + ] + } + } + }, + "user": {"type": "string"}, + "userns_mode": {"type": "string"}, + "volumes": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "required": ["type"], + "properties": { + "type": {"type": "string"}, + "source": {"type": "string"}, + "target": {"type": "string"}, + "read_only": {"type": "boolean"}, + "consistency": {"type": "string"}, + "bind": { + "type": "object", + "properties": { + "propagation": {"type": "string"} + } + }, + "volume": { + "type": "object", + "properties": { + "nocopy": {"type": "boolean"} + } + } + } + } + ], + "uniqueItems": true + } + }, + "working_dir": {"type": "string"} + }, + "additionalProperties": false + }, + + "healthcheck": { + "id": "#/definitions/healthcheck", + "type": "object", + "additionalProperties": false, + "properties": { + "disable": {"type": "boolean"}, + "interval": {"type": "string"}, + "retries": {"type": "number"}, + "test": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "timeout": {"type": "string"} + } + }, + "deployment": { + "id": "#/definitions/deployment", + "type": ["object", "null"], + "properties": { + "mode": {"type": "string"}, + "endpoint_mode": {"type": "string"}, + "replicas": {"type": "integer"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "update_config": { + "type": "object", + "properties": { + "parallelism": {"type": "integer"}, + "delay": {"type": "string", "format": "duration"}, + "failure_action": {"type": "string"}, + "monitor": {"type": "string", "format": "duration"}, + "max_failure_ratio": {"type": "number"} + }, + "additionalProperties": false + }, + "resources": { + "type": "object", + "properties": { + "limits": {"$ref": "#/definitions/resource"}, + "reservations": {"$ref": "#/definitions/resource"} + } + }, + "restart_policy": { + "type": "object", + "properties": { + "condition": {"type": "string"}, + "delay": {"type": "string", "format": "duration"}, + "max_attempts": {"type": "integer"}, + "window": {"type": "string", "format": "duration"} + }, + "additionalProperties": false + }, + "placement": { + "type": "object", + "properties": { + "constraints": {"type": "array", "items": {"type": "string"}}, + "preferences": { + "type": "array", + "items": { + "type": "object", + "properties": { + "spread": {"type": "string"} + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + + "resource": { + "id": "#/definitions/resource", + "type": "object", + "properties": { + "cpus": {"type": "string"}, + "memory": {"type": "string"} + }, + "additionalProperties": false + }, + + "network": { + "id": "#/definitions/network", + "type": ["object", "null"], + "properties": { + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "ipam": { + "type": "object", + "properties": { + "driver": {"type": "string"}, + "config": { + "type": "array", + "items": { + "type": "object", + "properties": { + "subnet": {"type": "string"} + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + }, + "additionalProperties": false + }, + "internal": {"type": "boolean"}, + "attachable": {"type": "boolean"}, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + }, + + "volume": { + "id": "#/definitions/volume", + "type": ["object", "null"], + "properties": { + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + }, + "additionalProperties": false + }, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + }, + + "secret": { + "id": "#/definitions/secret", + "type": "object", + "properties": { + "file": {"type": "string"}, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + } + }, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + }, + + "config": { + "id": "#/definitions/config", + "type": "object", + "properties": { + "file": {"type": "string"}, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + } + }, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + }, + + "string_or_list": { + "oneOf": [ + {"type": "string"}, + {"$ref": "#/definitions/list_of_strings"} + ] + }, + + "list_of_strings": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true + }, + + "list_or_dict": { + "oneOf": [ + { + "type": "object", + "patternProperties": { + ".+": { + "type": ["string", "number", "null"] + } + }, + "additionalProperties": false + }, + {"type": "array", "items": {"type": "string"}, "uniqueItems": true} + ] + }, + + "constraints": { + "service": { + "id": "#/definitions/constraints/service", + "anyOf": [ + {"required": ["build"]}, + {"required": ["image"]} + ], + "properties": { + "build": { + "required": ["context"] + } + } + } + } + } +} diff --git a/compose/config/serialize.py b/compose/config/serialize.py index 040973ae080..ac78b77a2db 100644 --- a/compose/config/serialize.py +++ b/compose/config/serialize.py @@ -10,6 +10,7 @@ from compose.const import COMPOSEFILE_V2_2 as V2_2 from compose.const import COMPOSEFILE_V3_1 as V3_1 from compose.const import COMPOSEFILE_V3_2 as V3_2 +from compose.const import COMPOSEFILE_V3_3 as V3_3 def serialize_config_type(dumper, data): @@ -50,7 +51,7 @@ def denormalize_config(config, image_digests=None): if 'external_name' in vol_conf: del vol_conf['external_name'] - if config.version in (V3_1, V3_2): + if config.version in (V3_1, V3_2, V3_3): result['secrets'] = config.secrets.copy() for secret_name, secret_conf in result['secrets'].items(): if 'external_name' in secret_conf: @@ -114,7 +115,7 @@ def denormalize_service_dict(service_dict, version, image_digest=None): service_dict['healthcheck']['timeout'] ) - if 'ports' in service_dict and version not in (V3_2,): + if 'ports' in service_dict and version not in (V3_2, V3_3): service_dict['ports'] = [ p.legacy_repr() if isinstance(p, types.ServicePort) else p for p in service_dict['ports'] diff --git a/compose/const.py b/compose/const.py index 36703138acb..36f213897a2 100644 --- a/compose/const.py +++ b/compose/const.py @@ -27,6 +27,7 @@ COMPOSEFILE_V3_0 = '3.0' COMPOSEFILE_V3_1 = '3.1' COMPOSEFILE_V3_2 = '3.2' +COMPOSEFILE_V3_3 = '3.3' API_VERSIONS = { COMPOSEFILE_V1: '1.21', @@ -36,6 +37,7 @@ COMPOSEFILE_V3_0: '1.25', COMPOSEFILE_V3_1: '1.25', COMPOSEFILE_V3_2: '1.25', + COMPOSEFILE_V3_3: '1.30', } API_VERSION_TO_ENGINE_VERSION = { @@ -46,4 +48,5 @@ API_VERSIONS[COMPOSEFILE_V3_0]: '1.13.0', API_VERSIONS[COMPOSEFILE_V3_1]: '1.13.0', API_VERSIONS[COMPOSEFILE_V3_2]: '1.13.0', + API_VERSIONS[COMPOSEFILE_V3_3]: '17.06.0', } diff --git a/docker-compose.spec b/docker-compose.spec index 21b3c1742cf..8e0d51ae5f8 100644 --- a/docker-compose.spec +++ b/docker-compose.spec @@ -52,6 +52,11 @@ exe = EXE(pyz, 'compose/config/config_schema_v3.2.json', 'DATA' ), + ( + 'compose/config/config_schema_v3.3.json', + 'compose/config/config_schema_v3.3.json', + 'DATA' + ), ( 'compose/GITSHA', 'compose/GITSHA', diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index bc51600357b..357244c2c33 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -31,6 +31,7 @@ from compose.const import COMPOSEFILE_V3_0 as V3_0 from compose.const import COMPOSEFILE_V3_1 as V3_1 from compose.const import COMPOSEFILE_V3_2 as V3_2 +from compose.const import COMPOSEFILE_V3_3 as V3_3 from compose.const import IS_WINDOWS_PLATFORM from compose.utils import nanoseconds_from_time_seconds from tests import mock @@ -825,11 +826,11 @@ def test_load_with_buildargs(self): assert service['build']['args']['opt1'] == '42' assert service['build']['args']['opt2'] == 'foobar' - def test_load_with_labels(self): + def test_load_with_build_labels(self): service = config.load( build_config_details( { - 'version': '3.2', + 'version': V3_3, 'services': { 'web': { 'build': {