summaryrefslogtreecommitdiff
path: root/compose/config
diff options
context:
space:
mode:
Diffstat (limited to 'compose/config')
-rw-r--r--compose/config/__init__.py2
-rw-r--r--compose/config/config.py270
-rw-r--r--compose/config/config_schema_v1.json17
-rw-r--r--compose/config/config_schema_v2.0.json20
-rw-r--r--compose/config/config_schema_v2.1.json30
-rw-r--r--compose/config/config_schema_v2.2.json34
-rw-r--r--compose/config/config_schema_v2.3.json91
-rw-r--r--compose/config/config_schema_v2.4.json513
-rw-r--r--compose/config/config_schema_v3.0.json25
-rw-r--r--compose/config/config_schema_v3.1.json27
-rw-r--r--compose/config/config_schema_v3.2.json28
-rw-r--r--compose/config/config_schema_v3.3.json32
-rw-r--r--compose/config/config_schema_v3.4.json32
-rw-r--r--compose/config/config_schema_v3.5.json88
-rw-r--r--compose/config/config_schema_v3.6.json582
-rw-r--r--compose/config/environment.py2
-rw-r--r--compose/config/interpolation.py221
-rw-r--r--compose/config/serialize.py28
-rw-r--r--compose/config/types.py167
-rw-r--r--compose/config/validation.py61
20 files changed, 2092 insertions, 178 deletions
diff --git a/compose/config/__init__.py b/compose/config/__init__.py
index b629edf6..e1032f3d 100644
--- a/compose/config/__init__.py
+++ b/compose/config/__init__.py
@@ -8,5 +8,7 @@ from .config import DOCKER_CONFIG_KEYS
from .config import find
from .config import load
from .config import merge_environment
+from .config import merge_labels
from .config import parse_environment
+from .config import parse_labels
from .config import resolve_build_args
diff --git a/compose/config/config.py b/compose/config/config.py
index d5aaf953..9f8a50c6 100644
--- a/compose/config/config.py
+++ b/compose/config/config.py
@@ -2,6 +2,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import functools
+import io
import logging
import os
import string
@@ -16,9 +17,11 @@ from . import types
from .. import const
from ..const import COMPOSEFILE_V1 as V1
from ..const import COMPOSEFILE_V2_1 as V2_1
+from ..const import COMPOSEFILE_V2_3 as V2_3
from ..const import COMPOSEFILE_V3_0 as V3_0
from ..const import COMPOSEFILE_V3_4 as V3_4
from ..utils import build_string_dict
+from ..utils import json_hash
from ..utils import parse_bytes
from ..utils import parse_nanoseconds_int
from ..utils import splitdrive
@@ -35,8 +38,10 @@ from .interpolation import interpolate_environment_variables
from .sort_services import get_container_name_from_network_mode
from .sort_services import get_service_name_from_network_mode
from .sort_services import sort_service_dicts
+from .types import MountSpec
from .types import parse_extra_hosts
from .types import parse_restart_spec
+from .types import SecurityOpt
from .types import ServiceLink
from .types import ServicePort
from .types import VolumeFromSpec
@@ -47,6 +52,7 @@ from .validation import validate_config_section
from .validation import validate_cpu
from .validation import validate_depends_on
from .validation import validate_extends_file_path
+from .validation import validate_healthcheck
from .validation import validate_links
from .validation import validate_network_mode
from .validation import validate_pid_mode
@@ -62,11 +68,15 @@ DOCKER_CONFIG_KEYS = [
'command',
'cpu_count',
'cpu_percent',
+ 'cpu_period',
'cpu_quota',
+ 'cpu_rt_period',
+ 'cpu_rt_runtime',
'cpu_shares',
'cpus',
'cpuset',
'detach',
+ 'device_cgroup_rules',
'devices',
'dns',
'dns_search',
@@ -90,11 +100,13 @@ DOCKER_CONFIG_KEYS = [
'mem_swappiness',
'net',
'oom_score_adj',
+ 'oom_kill_disable',
'pid',
'ports',
'privileged',
'read_only',
'restart',
+ 'runtime',
'secrets',
'security_opt',
'shm_size',
@@ -117,12 +129,14 @@ ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [
'container_name',
'credential_spec',
'dockerfile',
+ 'init',
'log_driver',
'log_opt',
'logging',
'network_mode',
- 'init',
+ 'platform',
'scale',
+ 'stop_grace_period',
]
DOCKER_VALID_URL_PREFIXES = (
@@ -335,7 +349,7 @@ def find_candidates_in_parent_dirs(filenames, path):
return (candidates, path)
-def check_swarm_only_config(service_dicts):
+def check_swarm_only_config(service_dicts, compatibility=False):
warning_template = (
"Some services ({services}) use the '{key}' key, which will be ignored. "
"Compose does not support '{key}' configuration - use "
@@ -351,13 +365,13 @@ def check_swarm_only_config(service_dicts):
key=key
)
)
-
- check_swarm_only_key(service_dicts, 'deploy')
+ if not compatibility:
+ check_swarm_only_key(service_dicts, 'deploy')
check_swarm_only_key(service_dicts, 'credential_spec')
check_swarm_only_key(service_dicts, 'configs')
-def load(config_details):
+def load(config_details, compatibility=False):
"""Load the configuration from a working directory and a list of
configuration files. Files are loaded in order, and merged on top
of each other to create the final configuration.
@@ -385,15 +399,17 @@ def load(config_details):
configs = load_mapping(
config_details.config_files, 'get_configs', 'Config', config_details.working_dir
)
- service_dicts = load_services(config_details, main_file)
+ service_dicts = load_services(config_details, main_file, compatibility)
if main_file.version != V1:
for service_dict in service_dicts:
match_named_volumes(service_dict, volumes)
- check_swarm_only_config(service_dicts)
+ check_swarm_only_config(service_dicts, compatibility)
+
+ version = V2_3 if compatibility and main_file.version >= V3_0 else main_file.version
- return Config(main_file.version, service_dicts, volumes, networks, secrets, configs)
+ return Config(version, service_dicts, volumes, networks, secrets, configs)
def load_mapping(config_files, get_func, entity_type, working_dir=None):
@@ -407,12 +423,11 @@ def load_mapping(config_files, get_func, entity_type, working_dir=None):
external = config.get('external')
if external:
- name_field = 'name' if entity_type == 'Volume' else 'external_name'
validate_external(entity_type, name, config, config_file.version)
if isinstance(external, dict):
- config[name_field] = external.get('name')
+ config['name'] = external.get('name')
elif not config.get('name'):
- config[name_field] = name
+ config['name'] = name
if 'driver_opts' in config:
config['driver_opts'] = build_string_dict(
@@ -436,7 +451,7 @@ def validate_external(entity_type, name, config, version):
entity_type, name, ', '.join(k for k in config if k != 'external')))
-def load_services(config_details, config_file):
+def load_services(config_details, config_file, compatibility=False):
def build_service(service_name, service_dict, service_names):
service_config = ServiceConfig.with_abs_paths(
config_details.working_dir,
@@ -454,7 +469,9 @@ def load_services(config_details, config_file):
service_config,
service_names,
config_file.version,
- config_details.environment)
+ config_details.environment,
+ compatibility
+ )
return service_dict
def build_services(service_config):
@@ -519,13 +536,13 @@ def process_config_file(config_file, environment, service_name=None):
processed_config['secrets'] = interpolate_config_section(
config_file,
config_file.get_secrets(),
- 'secrets',
+ 'secret',
environment)
if config_file.version >= const.COMPOSEFILE_V3_3:
processed_config['configs'] = interpolate_config_section(
config_file,
config_file.get_configs(),
- 'configs',
+ 'config',
environment
)
else:
@@ -686,6 +703,7 @@ def validate_service(service_config, service_names, config_file):
validate_pid_mode(service_config, service_names)
validate_depends_on(service_config, service_names)
validate_links(service_config, service_names)
+ validate_healthcheck(service_config)
if not service_dict.get('image') and has_uppercase(service_name):
raise ConfigurationError(
@@ -723,9 +741,9 @@ def process_service(service_config):
if field in service_dict:
service_dict[field] = to_list(service_dict[field])
- service_dict = process_blkio_config(process_ports(
- process_healthcheck(service_dict, service_config.name)
- ))
+ service_dict = process_security_opt(process_blkio_config(process_ports(
+ process_healthcheck(service_dict)
+ )))
return service_dict
@@ -788,37 +806,40 @@ def process_blkio_config(service_dict):
return service_dict
-def process_healthcheck(service_dict, service_name):
+def process_healthcheck(service_dict):
if 'healthcheck' not in service_dict:
return service_dict
- hc = {}
- raw = service_dict['healthcheck']
+ hc = service_dict['healthcheck']
- if raw.get('disable'):
- if len(raw) > 1:
- raise ConfigurationError(
- 'Service "{}" defines an invalid healthcheck: '
- '"disable: true" cannot be combined with other options'
- .format(service_name))
+ if 'disable' in hc:
+ del hc['disable']
hc['test'] = ['NONE']
- elif 'test' in raw:
- hc['test'] = raw['test']
for field in ['interval', 'timeout', 'start_period']:
- if field in raw:
- if not isinstance(raw[field], six.integer_types):
- hc[field] = parse_nanoseconds_int(raw[field])
- else: # Conversion has been done previously
- hc[field] = raw[field]
- if 'retries' in raw:
- hc['retries'] = raw['retries']
-
- service_dict['healthcheck'] = hc
+ if field not in hc or isinstance(hc[field], six.integer_types):
+ continue
+ hc[field] = parse_nanoseconds_int(hc[field])
+
return service_dict
-def finalize_service(service_config, service_names, version, environment):
+def finalize_service_volumes(service_dict, environment):
+ if 'volumes' in service_dict:
+ finalized_volumes = []
+ normalize = environment.get_boolean('COMPOSE_CONVERT_WINDOWS_PATHS')
+ win_host = environment.get_boolean('COMPOSE_FORCE_WINDOWS_HOST')
+ for v in service_dict['volumes']:
+ if isinstance(v, dict):
+ finalized_volumes.append(MountSpec.parse(v, normalize, win_host))
+ else:
+ finalized_volumes.append(VolumeSpec.parse(v, normalize, win_host))
+ service_dict['volumes'] = finalized_volumes
+
+ return service_dict
+
+
+def finalize_service(service_config, service_names, version, environment, compatibility):
service_dict = dict(service_config.config)
if 'environment' in service_dict or 'env_file' in service_dict:
@@ -831,12 +852,7 @@ def finalize_service(service_config, service_names, version, environment):
for vf in service_dict['volumes_from']
]
- if 'volumes' in service_dict:
- service_dict['volumes'] = [
- VolumeSpec.parse(
- v, environment.get_boolean('COMPOSE_CONVERT_WINDOWS_PATHS')
- ) for v in service_dict['volumes']
- ]
+ service_dict = finalize_service_volumes(service_dict, environment)
if 'net' in service_dict:
network_mode = service_dict.pop('net')
@@ -864,10 +880,80 @@ def finalize_service(service_config, service_names, version, environment):
normalize_build(service_dict, service_config.working_dir, environment)
+ if compatibility:
+ service_dict, ignored_keys = translate_deploy_keys_to_container_config(
+ service_dict
+ )
+ if ignored_keys:
+ log.warn(
+ 'The following deploy sub-keys are not supported in compatibility mode and have'
+ ' been ignored: {}'.format(', '.join(ignored_keys))
+ )
+
service_dict['name'] = service_config.name
return normalize_v1_service_format(service_dict)
+def translate_resource_keys_to_container_config(resources_dict, service_dict):
+ if 'limits' in resources_dict:
+ service_dict['mem_limit'] = resources_dict['limits'].get('memory')
+ if 'cpus' in resources_dict['limits']:
+ service_dict['cpus'] = float(resources_dict['limits']['cpus'])
+ if 'reservations' in resources_dict:
+ service_dict['mem_reservation'] = resources_dict['reservations'].get('memory')
+ if 'cpus' in resources_dict['reservations']:
+ return ['resources.reservations.cpus']
+ return []
+
+
+def convert_restart_policy(name):
+ try:
+ return {
+ 'any': 'always',
+ 'none': 'no',
+ 'on-failure': 'on-failure'
+ }[name]
+ except KeyError:
+ raise ConfigurationError('Invalid restart policy "{}"'.format(name))
+
+
+def translate_deploy_keys_to_container_config(service_dict):
+ if 'deploy' not in service_dict:
+ return service_dict, []
+
+ deploy_dict = service_dict['deploy']
+ ignored_keys = [
+ k for k in ['endpoint_mode', 'labels', 'update_config', 'placement']
+ if k in deploy_dict
+ ]
+
+ if 'replicas' in deploy_dict and deploy_dict.get('mode', 'replicated') == 'replicated':
+ service_dict['scale'] = deploy_dict['replicas']
+
+ if 'restart_policy' in deploy_dict:
+ service_dict['restart'] = {
+ 'Name': convert_restart_policy(deploy_dict['restart_policy'].get('condition', 'any')),
+ 'MaximumRetryCount': deploy_dict['restart_policy'].get('max_attempts', 0)
+ }
+ for k in deploy_dict['restart_policy'].keys():
+ if k != 'condition' and k != 'max_attempts':
+ ignored_keys.append('restart_policy.{}'.format(k))
+
+ ignored_keys.extend(
+ translate_resource_keys_to_container_config(
+ deploy_dict.get('resources', {}), service_dict
+ )
+ )
+
+ del service_dict['deploy']
+ if 'credential_spec' in service_dict:
+ del service_dict['credential_spec']
+ if 'configs' in service_dict:
+ del service_dict['configs']
+
+ return service_dict, ignored_keys
+
+
def normalize_v1_service_format(service_dict):
if 'log_driver' in service_dict or 'log_opt' in service_dict:
if 'logging' not in service_dict:
@@ -919,10 +1005,14 @@ class MergeDict(dict):
self.base.get(field, default),
self.override.get(field, default))
- def merge_mapping(self, field, parse_func):
+ def merge_mapping(self, field, parse_func=None):
if not self.needs_merge(field):
return
+ if parse_func is None:
+ def parse_func(m):
+ return m or {}
+
self[field] = parse_func(self.base.get(field))
self[field].update(parse_func(self.override.get(field)))
@@ -954,7 +1044,7 @@ def merge_service_dicts(base, override, version):
md.merge_sequence('links', ServiceLink.parse)
md.merge_sequence('secrets', types.ServiceSecret.parse)
md.merge_sequence('configs', types.ServiceConfig.parse)
- md.merge_mapping('deploy', parse_deploy)
+ md.merge_sequence('security_opt', types.SecurityOpt.parse)
md.merge_mapping('extra_hosts', parse_extra_hosts)
for field in ['volumes', 'devices']:
@@ -962,7 +1052,7 @@ def merge_service_dicts(base, override, version):
for field in [
'cap_add', 'cap_drop', 'expose', 'external_links',
- 'security_opt', 'volumes_from',
+ 'volumes_from', 'device_cgroup_rules',
]:
md.merge_field(field, merge_unique_items_lists, default=[])
@@ -973,6 +1063,7 @@ def merge_service_dicts(base, override, version):
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={})
for field in set(ALLOWED_KEYS) - set(md):
md.merge_scalar(field)
@@ -1029,12 +1120,49 @@ def merge_build(output, base, override):
md.merge_scalar('network')
md.merge_scalar('target')
md.merge_scalar('shm_size')
+ md.merge_scalar('isolation')
md.merge_mapping('args', parse_build_arguments)
md.merge_field('cache_from', merge_unique_items_lists, default=[])
md.merge_mapping('labels', parse_labels)
+ md.merge_mapping('extra_hosts', parse_extra_hosts)
+ return dict(md)
+
+
+def merge_deploy(base, override):
+ md = MergeDict(base or {}, override or {})
+ md.merge_scalar('mode')
+ md.merge_scalar('endpoint_mode')
+ md.merge_scalar('replicas')
+ md.merge_mapping('labels', parse_labels)
+ md.merge_mapping('update_config')
+ md.merge_mapping('restart_policy')
+ if md.needs_merge('resources'):
+ resources_md = MergeDict(md.base.get('resources') or {}, md.override.get('resources') or {})
+ resources_md.merge_mapping('limits')
+ resources_md.merge_field('reservations', merge_reservations, default={})
+ md['resources'] = dict(resources_md)
+ if md.needs_merge('placement'):
+ placement_md = MergeDict(md.base.get('placement') or {}, md.override.get('placement') or {})
+ placement_md.merge_field('constraints', merge_unique_items_lists, default=[])
+ placement_md.merge_field('preferences', merge_unique_objects_lists, default=[])
+ md['placement'] = dict(placement_md)
+
+ return dict(md)
+
+
+def merge_reservations(base, override):
+ md = MergeDict(base, override)
+ md.merge_scalar('cpus')
+ md.merge_scalar('memory')
+ md.merge_sequence('generic_resources', types.GenericResource.parse)
return dict(md)
+def merge_unique_objects_lists(base, override):
+ result = dict((json_hash(i), i) for i in base + override)
+ return [i[1] for i in sorted([(k, v) for k, v in result.items()], key=lambda x: x[0])]
+
+
def merge_blkio_config(base, override):
md = MergeDict(base, override)
md.merge_scalar('weight')
@@ -1084,6 +1212,12 @@ def merge_environment(base, override):
return env
+def merge_labels(base, override):
+ labels = parse_labels(base)
+ labels.update(parse_labels(override))
+ return labels
+
+
def split_kv(kvpair):
if '=' in kvpair:
return kvpair.split('=', 1)
@@ -1115,7 +1249,6 @@ parse_sysctls = functools.partial(parse_dict_or_list, split_kv, 'sysctls')
parse_depends_on = functools.partial(
parse_dict_or_list, lambda k: (k, {'condition': 'service_started'}), 'depends_on'
)
-parse_deploy = functools.partial(parse_dict_or_list, split_kv, 'deploy')
def parse_flat_dict(d):
@@ -1145,19 +1278,13 @@ def resolve_volume_paths(working_dir, service_dict):
def resolve_volume_path(working_dir, volume):
- mount_params = None
if isinstance(volume, dict):
- container_path = volume.get('target')
- host_path = volume.get('source')
- mode = None
- if host_path:
- if volume.get('read_only'):
- mode = 'ro'
- if volume.get('volume', {}).get('nocopy'):
- mode = 'nocopy'
- mount_params = (host_path, mode)
- else:
- container_path, mount_params = split_path_mapping(volume)
+ if volume.get('source', '').startswith('.') and volume['type'] == 'bind':
+ volume['source'] = expand_path(working_dir, volume['source'])
+ return volume
+
+ mount_params = None
+ container_path, mount_params = split_path_mapping(volume)
if mount_params is not None:
host_path, mode = mount_params
@@ -1258,6 +1385,16 @@ def split_path_mapping(volume_path):
return (volume_path, None)
+def process_security_opt(service_dict):
+ security_opts = service_dict.get('security_opt', [])
+ result = []
+ for value in security_opts:
+ result.append(SecurityOpt.parse(value))
+ if result:
+ service_dict['security_opt'] = result
+ return service_dict
+
+
def join_path_mapping(pair):
(container, host) = pair
if isinstance(host, dict):
@@ -1297,10 +1434,15 @@ def has_uppercase(name):
return any(char in string.ascii_uppercase for char in name)
-def load_yaml(filename):
+def load_yaml(filename, encoding=None):
try:
- with open(filename, 'r') as fh:
+ with io.open(filename, 'r', encoding=encoding) as fh:
return yaml.safe_load(fh)
- except (IOError, yaml.YAMLError) as e:
+ except (IOError, yaml.YAMLError, UnicodeDecodeError) as e:
+ if encoding is None:
+ # Sometimes the user's locale sets an encoding that doesn't match
+ # the YAML files. Im such cases, retry once with the "default"
+ # UTF-8 encoding
+ return load_yaml(filename, encoding='utf-8')
error_name = getattr(e, '__module__', '') + '.' + e.__class__.__name__
raise ConfigurationError(u"{}: {}".format(error_name, e))
diff --git a/compose/config/config_schema_v1.json b/compose/config/config_schema_v1.json
index 94354cda..2771f995 100644
--- a/compose/config/config_schema_v1.json
+++ b/compose/config/config_schema_v1.json
@@ -78,7 +78,7 @@
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"log_driver": {"type": "string"},
"log_opt": {"type": "object"},
@@ -166,6 +166,21 @@
]
},
+ "labels": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
diff --git a/compose/config/config_schema_v2.0.json b/compose/config/config_schema_v2.0.json
index 2ad62ac5..eddf787e 100644
--- a/compose/config/config_schema_v2.0.json
+++ b/compose/config/config_schema_v2.0.json
@@ -158,7 +158,7 @@
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
@@ -191,7 +191,8 @@
"properties": {
"aliases": {"$ref": "#/definitions/list_of_strings"},
"ipv4_address": {"type": "string"},
- "ipv6_address": {"type": "string"}
+ "ipv6_address": {"type": "string"},
+ "priority": {"type": "number"}
},
"additionalProperties": false
},
@@ -354,6 +355,21 @@
]
},
+ "labels": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
"blkio_limit": {
"type": "object",
"properties": {
diff --git a/compose/config/config_schema_v2.1.json b/compose/config/config_schema_v2.1.json
index 24e6ba02..5ad5a20e 100644
--- a/compose/config/config_schema_v2.1.json
+++ b/compose/config/config_schema_v2.1.json
@@ -88,7 +88,8 @@
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"},
+ "isolation": {"type": "string"}
},
"additionalProperties": false
}
@@ -106,6 +107,7 @@
"container_name": {"type": "string"},
"cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]},
+ "cpu_period": {"type": ["number", "string"]},
"cpuset": {"type": "string"},
"depends_on": {
"oneOf": [
@@ -183,7 +185,7 @@
"image": {"type": "string"},
"ipc": {"type": "string"},
"isolation": {"type": "string"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
@@ -217,7 +219,8 @@
"aliases": {"$ref": "#/definitions/list_of_strings"},
"ipv4_address": {"type": "string"},
"ipv6_address": {"type": "string"},
- "link_local_ips": {"$ref": "#/definitions/list_of_strings"}
+ "link_local_ips": {"$ref": "#/definitions/list_of_strings"},
+ "priority": {"type": "number"}
},
"additionalProperties": false
},
@@ -229,6 +232,7 @@
}
]
},
+ "oom_kill_disable": {"type": "boolean"},
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
"group_add": {
"type": "array",
@@ -349,7 +353,8 @@
},
"internal": {"type": "boolean"},
"enable_ipv6": {"type": "boolean"},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"},
+ "name": {"type": "string"}
},
"additionalProperties": false
},
@@ -372,7 +377,7 @@
},
"additionalProperties": false
},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"name": {"type": "string"}
},
"additionalProperties": false
@@ -406,6 +411,21 @@
]
},
+ "labels": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
"blkio_limit": {
"type": "object",
"properties": {
diff --git a/compose/config/config_schema_v2.2.json b/compose/config/config_schema_v2.2.json
index 86fc5df9..26044b65 100644
--- a/compose/config/config_schema_v2.2.json
+++ b/compose/config/config_schema_v2.2.json
@@ -88,9 +88,10 @@
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"cache_from": {"$ref": "#/definitions/list_of_strings"},
- "network": {"type": "string"}
+ "network": {"type": "string"},
+ "isolation": {"type": "string"}
},
"additionalProperties": false
}
@@ -110,6 +111,9 @@
"cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100},
"cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]},
+ "cpu_period": {"type": ["number", "string"]},
+ "cpu_rt_period": {"type": ["number", "string"]},
+ "cpu_rt_runtime": {"type": ["number", "string"]},
"cpus": {"type": "number", "minimum": 0},
"cpuset": {"type": "string"},
"depends_on": {
@@ -189,7 +193,7 @@
"init": {"type": ["boolean", "string"]},
"ipc": {"type": "string"},
"isolation": {"type": "string"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
@@ -223,7 +227,8 @@
"aliases": {"$ref": "#/definitions/list_of_strings"},
"ipv4_address": {"type": "string"},
"ipv6_address": {"type": "string"},
- "link_local_ips": {"$ref": "#/definitions/list_of_strings"}
+ "link_local_ips": {"$ref": "#/definitions/list_of_strings"},
+ "priority": {"type": "number"}
},
"additionalProperties": false
},
@@ -235,6 +240,7 @@
}
]
},
+ "oom_kill_disable": {"type": "boolean"},
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
"group_add": {
"type": "array",
@@ -356,7 +362,8 @@
},
"internal": {"type": "boolean"},
"enable_ipv6": {"type": "boolean"},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"},
+ "name": {"type": "string"}
},
"additionalProperties": false
},
@@ -379,7 +386,7 @@
},
"additionalProperties": false
},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"name": {"type": "string"}
},
"additionalProperties": false
@@ -413,6 +420,21 @@
]
},
+ "labels": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
"blkio_limit": {
"type": "object",
"properties": {
diff --git a/compose/config/config_schema_v2.3.json b/compose/config/config_schema_v2.3.json
index ceaf4495..ac0778f2 100644
--- a/compose/config/config_schema_v2.3.json
+++ b/compose/config/config_schema_v2.3.json
@@ -88,18 +88,20 @@
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"cache_from": {"$ref": "#/definitions/list_of_strings"},
"network": {"type": "string"},
"target": {"type": "string"},
- "shm_size": {"type": ["integer", "string"]}
+ "shm_size": {"type": ["integer", "string"]},
+ "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
+ "isolation": {"type": "string"}
},
"additionalProperties": false
}
]
},
- "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
- "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "cap_add": {"$ref": "#/definitions/list_of_strings"},
+ "cap_drop": {"$ref": "#/definitions/list_of_strings"},
"cgroup_parent": {"type": "string"},
"command": {
"oneOf": [
@@ -112,6 +114,9 @@
"cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100},
"cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]},
+ "cpu_period": {"type": ["number", "string"]},
+ "cpu_rt_period": {"type": ["number", "string"]},
+ "cpu_rt_runtime": {"type": ["number", "string"]},
"cpus": {"type": "number", "minimum": 0},
"cpuset": {"type": "string"},
"depends_on": {
@@ -136,7 +141,8 @@
}
]
},
- "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "device_cgroup_rules": {"$ref": "#/definitions/list_of_strings"},
+ "devices": {"$ref": "#/definitions/list_of_strings"},
"dns_opt": {
"type": "array",
"items": {
@@ -183,7 +189,7 @@
]
},
- "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "external_links": {"$ref": "#/definitions/list_of_strings"},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"healthcheck": {"$ref": "#/definitions/healthcheck"},
"hostname": {"type": "string"},
@@ -191,8 +197,8 @@
"init": {"type": ["boolean", "string"]},
"ipc": {"type": "string"},
"isolation": {"type": "string"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
- "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "labels": {"$ref": "#/definitions/labels"},
+ "links": {"$ref": "#/definitions/list_of_strings"},
"logging": {
"type": "object",
@@ -225,7 +231,8 @@
"aliases": {"$ref": "#/definitions/list_of_strings"},
"ipv4_address": {"type": "string"},
"ipv6_address": {"type": "string"},
- "link_local_ips": {"$ref": "#/definitions/list_of_strings"}
+ "link_local_ips": {"$ref": "#/definitions/list_of_strings"},
+ "priority": {"type": "number"}
},
"additionalProperties": false
},
@@ -237,6 +244,7 @@
}
]
},
+ "oom_kill_disable": {"type": "boolean"},
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
"group_add": {
"type": "array",
@@ -259,8 +267,9 @@
"privileged": {"type": "boolean"},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
+ "runtime": {"type": "string"},
"scale": {"type": "integer"},
- "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "security_opt": {"$ref": "#/definitions/list_of_strings"},
"shm_size": {"type": ["number", "string"]},
"sysctls": {"$ref": "#/definitions/list_or_dict"},
"pids_limit": {"type": ["number", "string"]},
@@ -291,9 +300,47 @@
},
"user": {"type": "string"},
"userns_mode": {"type": "string"},
- "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "volumes": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {"type": "string"},
+ {
+ "type": "object",
+ "required": ["type"],
+ "additionalProperties": false,
+ "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"}
+ }
+ },
+ "tmpfs": {
+ "type": "object",
+ "properties": {
+ "size": {"type": ["integer", "string"]}
+ }
+ }
+ }
+ }
+ ],
+ "uniqueItems": true
+ }
+ },
"volume_driver": {"type": "string"},
- "volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "volumes_from": {"$ref": "#/definitions/list_of_strings"},
"working_dir": {"type": "string"}
},
@@ -359,7 +406,8 @@
},
"internal": {"type": "boolean"},
"enable_ipv6": {"type": "boolean"},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"},
+ "name": {"type": "string"}
},
"additionalProperties": false
},
@@ -382,7 +430,7 @@
},
"additionalProperties": false
},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"name": {"type": "string"}
},
"additionalProperties": false
@@ -416,6 +464,21 @@
]
},
+ "labels": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
"blkio_limit": {
"type": "object",
"properties": {
diff --git a/compose/config/config_schema_v2.4.json b/compose/config/config_schema_v2.4.json
new file mode 100644
index 00000000..731fa2f9
--- /dev/null
+++ b/compose/config/config_schema_v2.4.json
@@ -0,0 +1,513 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "id": "config_schema_v2.4.json",
+ "type": "object",
+
+ "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
+ }
+ },
+
+ "patternProperties": {"^x-": {}},
+ "additionalProperties": false,
+
+ "definitions": {
+
+ "service": {
+ "id": "#/definitions/service",
+ "type": "object",
+
+ "properties": {
+ "blkio_config": {
+ "type": "object",
+ "properties": {
+ "device_read_bps": {
+ "type": "array",
+ "items": {"$ref": "#/definitions/blkio_limit"}
+ },
+ "device_read_iops": {
+ "type": "array",
+ "items": {"$ref": "#/definitions/blkio_limit"}
+ },
+ "device_write_bps": {
+ "type": "array",
+ "items": {"$ref": "#/definitions/blkio_limit"}
+ },
+ "device_write_iops": {
+ "type": "array",
+ "items": {"$ref": "#/definitions/blkio_limit"}
+ },
+ "weight": {"type": "integer"},
+ "weight_device": {
+ "type": "array",
+ "items": {"$ref": "#/definitions/blkio_weight"}
+ }
+ },
+ "additionalProperties": false
+ },
+
+ "build": {
+ "oneOf": [
+ {"type": "string"},
+ {
+ "type": "object",
+ "properties": {
+ "context": {"type": "string"},
+ "dockerfile": {"type": "string"},
+ "args": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
+ "cache_from": {"$ref": "#/definitions/list_of_strings"},
+ "network": {"type": "string"},
+ "target": {"type": "string"},
+ "shm_size": {"type": ["integer", "string"]},
+ "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
+ "isolation": {"type": "string"}
+ },
+ "additionalProperties": false
+ }
+ ]
+ },
+ "cap_add": {"$ref": "#/definitions/list_of_strings"},
+ "cap_drop": {"$ref": "#/definitions/list_of_strings"},
+ "cgroup_parent": {"type": "string"},
+ "command": {
+ "oneOf": [
+ {"type": "string"},
+ {"type": "array", "items": {"type": "string"}}
+ ]
+ },
+ "container_name": {"type": "string"},
+ "cpu_count": {"type": "integer", "minimum": 0},
+ "cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100},
+ "cpu_shares": {"type": ["number", "string"]},
+ "cpu_quota": {"type": ["number", "string"]},
+ "cpu_period": {"type": ["number", "string"]},
+ "cpu_rt_period": {"type": ["number", "string"]},
+ "cpu_rt_runtime": {"type": ["number", "string"]},
+ "cpus": {"type": "number", "minimum": 0},
+ "cpuset": {"type": "string"},
+ "depends_on": {
+ "oneOf": [
+ {"$ref": "#/definitions/list_of_strings"},
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-zA-Z0-9._-]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "condition": {
+ "type": "string",
+ "enum": ["service_started", "service_healthy"]
+ }
+ },
+ "required": ["condition"]
+ }
+ }
+ }
+ ]
+ },
+ "device_cgroup_rules": {"$ref": "#/definitions/list_of_strings"},
+ "devices": {"$ref": "#/definitions/list_of_strings"},
+ "dns_opt": {
+ "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
+ },
+
+ "extends": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "object",
+
+ "properties": {
+ "service": {"type": "string"},
+ "file": {"type": "string"}
+ },
+ "required": ["service"],
+ "additionalProperties": false
+ }
+ ]
+ },
+
+ "external_links": {"$ref": "#/definitions/list_of_strings"},
+ "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
+ "group_add": {
+ "type": "array",
+ "items": {
+ "type": ["string", "number"]
+ },
+ "uniqueItems": true
+ },
+ "healthcheck": {"$ref": "#/definitions/healthcheck"},
+ "hostname": {"type": "string"},
+ "image": {"type": "string"},
+ "init": {"type": ["boolean", "string"]},
+ "ipc": {"type": "string"},
+ "isolation": {"type": "string"},
+ "labels": {"$ref": "#/definitions/labels"},
+ "links": {"$ref": "#/definitions/list_of_strings"},
+
+ "logging": {
+ "type": "object",
+
+ "properties": {
+ "driver": {"type": "string"},
+ "options": {"type": "object"}
+ },
+ "additionalProperties": false
+ },
+
+ "mac_address": {"type": "string"},
+ "mem_limit": {"type": ["number", "string"]},
+ "mem_reservation": {"type": ["string", "integer"]},
+ "mem_swappiness": {"type": "integer"},
+ "memswap_limit": {"type": ["number", "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"},
+ "link_local_ips": {"$ref": "#/definitions/list_of_strings"},
+ "priority": {"type": "number"}
+ },
+ "additionalProperties": false
+ },
+ {"type": "null"}
+ ]
+ }
+ },
+ "additionalProperties": false
+ }
+ ]
+ },
+ "oom_kill_disable": {"type": "boolean"},
+ "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
+ "pid": {"type": ["string", "null"]},
+ "platform": {"type": "string"},
+ "ports": {
+ "type": "array",
+ "items": {
+ "type": ["string", "number"],
+ "format": "ports"
+ },
+ "uniqueItems": true
+ },
+ "privileged": {"type": "boolean"},
+ "read_only": {"type": "boolean"},
+ "restart": {"type": "string"},
+ "runtime": {"type": "string"},
+ "scale": {"type": "integer"},
+ "security_opt": {"$ref": "#/definitions/list_of_strings"},
+ "shm_size": {"type": ["number", "string"]},
+ "sysctls": {"$ref": "#/definitions/list_or_dict"},
+ "pids_limit": {"type": ["number", "string"]},
+ "stdin_open": {"type": "boolean"},
+ "stop_grace_period": {"type": "string", "format": "duration"},
+ "stop_signal": {"type": "string"},
+ "storage_opt": {"type": "object"},
+ "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"],
+ "additionalProperties": false,
+ "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"}
+ }
+ },
+ "tmpfs": {
+ "type": "object",
+ "properties": {
+ "size": {"type": ["integer", "string"]}
+ }
+ }
+ }
+ }
+ ],
+ "uniqueItems": true
+ }
+ },
+ "volume_driver": {"type": "string"},
+ "volumes_from": {"$ref": "#/definitions/list_of_strings"},
+ "working_dir": {"type": "string"}
+ },
+
+ "dependencies": {
+ "memswap_limit": ["mem_limit"]
+ },
+ "additionalProperties": false
+ },
+
+ "healthcheck": {
+ "id": "#/definitions/healthcheck",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "disable": {"type": "boolean"},
+ "interval": {"type": "string"},
+ "retries": {"type": "number"},
+ "start_period": {"type": "string"},
+ "test": {
+ "oneOf": [
+ {"type": "string"},
+ {"type": "array", "items": {"type": "string"}}
+ ]
+ },
+ "timeout": {"type": "string"}
+ }
+ },
+
+ "network": {
+ "id": "#/definitions/network",
+ "type": "object",
+ "properties": {
+ "driver": {"type": "string"},
+ "driver_opts": {
+ "type": "object",
+ "patternProperties": {
+ "^.+$": {"type": ["string", "number"]}
+ }
+ },
+ "ipam": {
+ "type": "object",
+ "properties": {
+ "driver": {"type": "string"},
+ "config": {
+ "type": "array"
+ },
+ "options": {
+ "type": "object",
+ "patternProperties": {
+ "^.+$": {"type": "string"}
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "external": {
+ "type": ["boolean", "object"],
+ "properties": {
+ "name": {"type": "string"}
+ },
+ "additionalProperties": false
+ },
+ "internal": {"type": "boolean"},
+ "enable_ipv6": {"type": "boolean"},
+ "labels": {"$ref": "#/definitions/labels"},
+ "name": {"type": "string"}
+ },
+ "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/labels"},
+ "name": {"type": "string"}
+ },
+ "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}
+ ]
+ },
+
+ "labels": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
+ "blkio_limit": {
+ "type": "object",
+ "properties": {
+ "path": {"type": "string"},
+ "rate": {"type": ["integer", "string"]}
+ },
+ "additionalProperties": false
+ },
+ "blkio_weight": {
+ "type": "object",
+ "properties": {
+ "path": {"type": "string"},
+ "weight": {"type": "integer"}
+ },
+ "additionalProperties": false
+ },
+
+ "constraints": {
+ "service": {
+ "id": "#/definitions/constraints/service",
+ "anyOf": [
+ {"required": ["build"]},
+ {"required": ["image"]}
+ ],
+ "properties": {
+ "build": {
+ "required": ["context"]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/compose/config/config_schema_v3.0.json b/compose/config/config_schema_v3.0.json
index f39344cf..10c36352 100644
--- a/compose/config/config_schema_v3.0.json
+++ b/compose/config/config_schema_v3.0.json
@@ -105,7 +105,7 @@
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
@@ -223,7 +223,7 @@
"properties": {
"mode": {"type": "string"},
"replicas": {"type": "integer"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"update_config": {
"type": "object",
"properties": {
@@ -294,7 +294,7 @@
"items": {
"type": "object",
"properties": {
- "subnet": {"type": "string"}
+ "subnet": {"type": "string", "format": "subnet_ip_address"}
},
"additionalProperties": false
}
@@ -310,7 +310,7 @@
"additionalProperties": false
},
"internal": {"type": "boolean"},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -333,7 +333,7 @@
},
"additionalProperties": false
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -366,6 +366,21 @@
]
},
+ "labels": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
diff --git a/compose/config/config_schema_v3.1.json b/compose/config/config_schema_v3.1.json
index 719c0fa7..8630ec31 100644
--- a/compose/config/config_schema_v3.1.json
+++ b/compose/config/config_schema_v3.1.json
@@ -116,7 +116,7 @@
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
@@ -252,7 +252,7 @@
"properties": {
"mode": {"type": "string"},
"replicas": {"type": "integer"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"update_config": {
"type": "object",
"properties": {
@@ -323,7 +323,7 @@
"items": {
"type": "object",
"properties": {
- "subnet": {"type": "string"}
+ "subnet": {"type": "string", "format": "subnet_ip_address"}
},
"additionalProperties": false
}
@@ -339,7 +339,7 @@
"additionalProperties": false
},
"internal": {"type": "boolean"},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -362,7 +362,7 @@
},
"additionalProperties": false
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -378,7 +378,7 @@
"name": {"type": "string"}
}
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -411,6 +411,21 @@
]
},
+ "labels": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
diff --git a/compose/config/config_schema_v3.2.json b/compose/config/config_schema_v3.2.json
index 2ca8e92d..5eccdea7 100644
--- a/compose/config/config_schema_v3.2.json
+++ b/compose/config/config_schema_v3.2.json
@@ -118,7 +118,7 @@
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
@@ -245,6 +245,7 @@
{
"type": "object",
"required": ["type"],
+ "additionalProperties": false,
"properties": {
"type": {"type": "string"},
"source": {"type": "string"},
@@ -298,7 +299,7 @@
"mode": {"type": "string"},
"endpoint_mode": {"type": "string"},
"replicas": {"type": "integer"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"update_config": {
"type": "object",
"properties": {
@@ -369,7 +370,7 @@
"items": {
"type": "object",
"properties": {
- "subnet": {"type": "string"}
+ "subnet": {"type": "string", "format": "subnet_ip_address"}
},
"additionalProperties": false
}
@@ -386,7 +387,7 @@
},
"internal": {"type": "boolean"},
"attachable": {"type": "boolean"},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -409,7 +410,7 @@
},
"additionalProperties": false
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -425,7 +426,7 @@
"name": {"type": "string"}
}
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -458,6 +459,21 @@
]
},
+ "labels": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
diff --git a/compose/config/config_schema_v3.3.json b/compose/config/config_schema_v3.3.json
index f1eb9a66..f63842b9 100644
--- a/compose/config/config_schema_v3.3.json
+++ b/compose/config/config_schema_v3.3.json
@@ -83,7 +83,7 @@
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"cache_from": {"$ref": "#/definitions/list_of_strings"}
},
"additionalProperties": false
@@ -151,7 +151,7 @@
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
@@ -278,6 +278,7 @@
{
"type": "object",
"required": ["type"],
+ "additionalProperties": false,
"properties": {
"type": {"type": "string"},
"source": {"type": "string"},
@@ -331,7 +332,7 @@
"mode": {"type": "string"},
"endpoint_mode": {"type": "string"},
"replicas": {"type": "integer"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"update_config": {
"type": "object",
"properties": {
@@ -412,7 +413,7 @@
"items": {
"type": "object",
"properties": {
- "subnet": {"type": "string"}
+ "subnet": {"type": "string", "format": "subnet_ip_address"}
},
"additionalProperties": false
}
@@ -429,7 +430,7 @@
},
"internal": {"type": "boolean"},
"attachable": {"type": "boolean"},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -452,7 +453,7 @@
},
"additionalProperties": false
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -468,7 +469,7 @@
"name": {"type": "string"}
}
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -484,7 +485,7 @@
"name": {"type": "string"}
}
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -517,6 +518,21 @@
]
},
+ "labels": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
diff --git a/compose/config/config_schema_v3.4.json b/compose/config/config_schema_v3.4.json
index dae7d7d2..23e95544 100644
--- a/compose/config/config_schema_v3.4.json
+++ b/compose/config/config_schema_v3.4.json
@@ -85,7 +85,7 @@
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"cache_from": {"$ref": "#/definitions/list_of_strings"},
"network": {"type": "string"},
"target": {"type": "string"}
@@ -155,7 +155,7 @@
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
@@ -282,6 +282,7 @@
{
"type": "object",
"required": ["type"],
+ "additionalProperties": false,
"properties": {
"type": {"type": "string"},
"source": {"type": "string"},
@@ -336,7 +337,7 @@
"mode": {"type": "string"},
"endpoint_mode": {"type": "string"},
"replicas": {"type": "integer"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"update_config": {
"type": "object",
"properties": {
@@ -420,7 +421,7 @@
"items": {
"type": "object",
"properties": {
- "subnet": {"type": "string"}
+ "subnet": {"type": "string", "format": "subnet_ip_address"}
},
"additionalProperties": false
}
@@ -437,7 +438,7 @@
},
"internal": {"type": "boolean"},
"attachable": {"type": "boolean"},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -461,7 +462,7 @@
},
"additionalProperties": false
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -477,7 +478,7 @@
"name": {"type": "string"}
}
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -493,7 +494,7 @@
"name": {"type": "string"}
}
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -526,6 +527,21 @@
]
},
+ "labels": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
diff --git a/compose/config/config_schema_v3.5.json b/compose/config/config_schema_v3.5.json
index fa95d6a2..e3bdecbc 100644
--- a/compose/config/config_schema_v3.5.json
+++ b/compose/config/config_schema_v3.5.json
@@ -64,6 +64,7 @@
}
},
+ "patternProperties": {"^x-": {}},
"additionalProperties": false,
"definitions": {
@@ -83,7 +84,7 @@
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"cache_from": {"$ref": "#/definitions/list_of_strings"},
"network": {"type": "string"},
"target": {"type": "string"},
@@ -154,7 +155,8 @@
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "isolation": {"type": "string"},
+ "labels": {"$ref": "#/definitions/labels"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
@@ -299,7 +301,8 @@
"nocopy": {"type": "boolean"}
}
}
- }
+ },
+ "additionalProperties": false
}
],
"uniqueItems": true
@@ -316,7 +319,7 @@
"additionalProperties": false,
"properties": {
"disable": {"type": "boolean"},
- "interval": {"type": "string"},
+ "interval": {"type": "string", "format": "duration"},
"retries": {"type": "number"},
"test": {
"oneOf": [
@@ -324,7 +327,8 @@
{"type": "array", "items": {"type": "string"}}
]
},
- "timeout": {"type": "string"}
+ "timeout": {"type": "string", "format": "duration"},
+ "start_period": {"type": "string", "format": "duration"}
}
},
"deployment": {
@@ -334,7 +338,7 @@
"mode": {"type": "string"},
"endpoint_mode": {"type": "string"},
"replicas": {"type": "integer"},
- "labels": {"$ref": "#/definitions/list_or_dict"},
+ "labels": {"$ref": "#/definitions/labels"},
"update_config": {
"type": "object",
"properties": {
@@ -352,8 +356,23 @@
"resources": {
"type": "object",
"properties": {
- "limits": {"$ref": "#/definitions/resource"},
- "reservations": {"$ref": "#/definitions/resource"}
+ "limits": {
+ "type": "object",
+ "properties": {
+ "cpus": {"type": "string"},
+ "memory": {"type": "string"}
+ },
+ "additionalProperties": false
+ },
+ "reservations": {
+ "type": "object",
+ "properties": {
+ "cpus": {"type": "string"},
+ "memory": {"type": "string"},
+ "generic_resources": {"$ref": "#/definitions/generic_resources"}
+ },
+ "additionalProperties": false
+ }
},
"additionalProperties": false
},
@@ -388,20 +407,30 @@
"additionalProperties": false
},
- "resource": {
- "id": "#/definitions/resource",
- "type": "object",
- "properties": {
- "cpus": {"type": "string"},
- "memory": {"type": "string"}
- },
- "additionalProperties": false
+ "generic_resources": {
+ "id": "#/definitions/generic_resources",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "discrete_resource_spec": {
+ "type": "object",
+ "properties": {
+ "kind": {"type": "string"},
+ "value": {"type": "number"}
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
},
"network": {
"id": "#/definitions/network",
"type": ["object", "null"],
"properties": {
+ "name": {"type": "string"},
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
@@ -418,7 +447,7 @@
"items": {
"type": "object",
"properties": {
- "subnet": {"type": "string"}
+ "subnet": {"type": "string", "format": "subnet_ip_address"}
},
"additionalProperties": false
}
@@ -435,7 +464,7 @@
},
"internal": {"type": "boolean"},
"attachable": {"type": "boolean"},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -459,7 +488,7 @@
},
"additionalProperties": false
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -468,6 +497,7 @@
"id": "#/definitions/secret",
"type": "object",
"properties": {
+ "name": {"type": "string"},
"file": {"type": "string"},
"external": {
"type": ["boolean", "object"],
@@ -475,7 +505,7 @@
"name": {"type": "string"}
}
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -484,6 +514,7 @@
"id": "#/definitions/config",
"type": "object",
"properties": {
+ "name": {"type": "string"},
"file": {"type": "string"},
"external": {
"type": ["boolean", "object"],
@@ -491,7 +522,7 @@
"name": {"type": "string"}
}
},
- "labels": {"$ref": "#/definitions/list_or_dict"}
+ "labels": {"$ref": "#/definitions/labels"}
},
"additionalProperties": false
},
@@ -524,6 +555,21 @@
]
},
+ "labels": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
diff --git a/compose/config/config_schema_v3.6.json b/compose/config/config_schema_v3.6.json
new file mode 100644
index 00000000..95a552b3
--- /dev/null
+++ b/compose/config/config_schema_v3.6.json
@@ -0,0 +1,582 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "id": "config_schema_v3.6.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
+ }
+ },
+
+ "patternProperties": {"^x-": {}},
+ "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"},
+ "network": {"type": "string"},
+ "target": {"type": "string"},
+ "shm_size": {"type": ["integer", "string"]}
+ },
+ "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"},
+ "isolation": {"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"}
+ }
+ },
+ "tmpfs": {
+ "type": "object",
+ "properties": {
+ "size": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ }
+ ],
+ "uniqueItems": true
+ }
+ },
+ "working_dir": {"type": "string"}
+ },
+ "additionalProperties": false
+ },
+
+ "healthcheck": {
+ "id": "#/definitions/healthcheck",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "disable": {"type": "boolean"},
+ "interval": {"type": "string", "format": "duration"},
+ "retries": {"type": "number"},
+ "test": {
+ "oneOf": [
+ {"type": "string"},
+ {"type": "array", "items": {"type": "string"}}
+ ]
+ },
+ "timeout": {"type": "string", "format": "duration"},
+ "start_period": {"type": "string", "format": "duration"}
+ }
+ },
+ "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"},
+ "order": {"type": "string", "enum": [
+ "start-first", "stop-first"
+ ]}
+ },
+ "additionalProperties": false
+ },
+ "resources": {
+ "type": "object",
+ "properties": {
+ "limits": {
+ "type": "object",
+ "properties": {
+ "cpus": {"type": "string"},
+ "memory": {"type": "string"}
+ },
+ "additionalProperties": false
+ },
+ "reservations": {
+ "type": "object",
+ "properties": {
+ "cpus": {"type": "string"},
+ "memory": {"type": "string"},
+ "generic_resources": {"$ref": "#/definitions/generic_resources"}
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "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
+ },
+
+ "generic_resources": {
+ "id": "#/definitions/generic_resources",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "discrete_resource_spec": {
+ "type": "object",
+ "properties": {
+ "kind": {"type": "string"},
+ "value": {"type": "number"}
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+
+ "network": {
+ "id": "#/definitions/network",
+ "type": ["object", "null"],
+ "properties": {
+ "name": {"type": "string"},
+ "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": {
+ "name": {"type": "string"},
+ "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": {
+ "name": {"type": "string"},
+ "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": {
+ "name": {"type": "string"},
+ "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/environment.py b/compose/config/environment.py
index 4ba228c8..0087b612 100644
--- a/compose/config/environment.py
+++ b/compose/config/environment.py
@@ -32,7 +32,7 @@ def env_vars_from_file(filename):
elif not os.path.isfile(filename):
raise ConfigurationError("%s is not a file." % (filename))
env = {}
- with contextlib.closing(codecs.open(filename, 'r', 'utf-8')) as fileobj:
+ with contextlib.closing(codecs.open(filename, 'r', 'utf-8-sig')) as fileobj:
for line in fileobj:
line = line.strip()
if line and not line.startswith('#'):
diff --git a/compose/config/interpolation.py b/compose/config/interpolation.py
index b13ac591..8845d73b 100644
--- a/compose/config/interpolation.py
+++ b/compose/config/interpolation.py
@@ -2,12 +2,15 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import logging
+import re
from string import Template
import six
from .errors import ConfigurationError
from compose.const import COMPOSEFILE_V2_0 as V2_0
+from compose.utils import parse_bytes
+from compose.utils import parse_nanoseconds_int
log = logging.getLogger(__name__)
@@ -44,9 +47,13 @@ def interpolate_environment_variables(version, config, section, environment):
)
+def get_config_path(config_key, section, name):
+ return '{}.{}.{}'.format(section, name, config_key)
+
+
def interpolate_value(name, config_key, value, section, interpolator):
try:
- return recursive_interpolate(value, interpolator)
+ return recursive_interpolate(value, interpolator, get_config_path(config_key, section, name))
except InvalidInterpolation as e:
raise ConfigurationError(
'Invalid interpolation format for "{config_key}" option '
@@ -55,38 +62,84 @@ def interpolate_value(name, config_key, value, section, interpolator):
name=name,
section=section,
string=e.string))
+ except UnsetRequiredSubstitution as e:
+ raise ConfigurationError(
+ 'Missing mandatory value for "{config_key}" option in {section} "{name}": {err}'.format(
+ config_key=config_key,
+ name=name,
+ section=section,
+ err=e.err
+ )
+ )
+
+def recursive_interpolate(obj, interpolator, config_path):
+ def append(config_path, key):
+ return '{}.{}'.format(config_path, key)
-def recursive_interpolate(obj, interpolator):
if isinstance(obj, six.string_types):
- return interpolator.interpolate(obj)
+ return converter.convert(config_path, interpolator.interpolate(obj))
if isinstance(obj, dict):
return dict(
- (key, recursive_interpolate(val, interpolator))
+ (key, recursive_interpolate(val, interpolator, append(config_path, key)))
for (key, val) in obj.items()
)
if isinstance(obj, list):
- return [recursive_interpolate(val, interpolator) for val in obj]
- return obj
+ return [recursive_interpolate(val, interpolator, config_path) for val in obj]
+ return converter.convert(config_path, obj)
class TemplateWithDefaults(Template):
- idpattern = r'[_a-z][_a-z0-9]*(?::?-[^}]+)?'
+ pattern = r"""
+ %(delim)s(?:
+ (?P<escaped>%(delim)s) |
+ (?P<named>%(id)s) |
+ {(?P<braced>%(bid)s)} |
+ (?P<invalid>)
+ )
+ """ % {
+ 'delim': re.escape('$'),
+ 'id': r'[_a-z][_a-z0-9]*',
+ 'bid': r'[_a-z][_a-z0-9]*(?:(?P<sep>:?[-?])[^}]*)?',
+ }
+
+ @staticmethod
+ def process_braced_group(braced, sep, mapping):
+ if ':-' == sep:
+ var, _, default = braced.partition(':-')
+ return mapping.get(var) or default
+ elif '-' == sep:
+ var, _, default = braced.partition('-')
+ return mapping.get(var, default)
+
+ elif ':?' == sep:
+ var, _, err = braced.partition(':?')
+ result = mapping.get(var)
+ if not result:
+ raise UnsetRequiredSubstitution(err)
+ return result
+ elif '?' == sep:
+ var, _, err = braced.partition('?')
+ if var in mapping:
+ return mapping.get(var)
+ raise UnsetRequiredSubstitution(err)
# Modified from python2.7/string.py
def substitute(self, mapping):
# Helper function for .sub()
+
def convert(mo):
- # Check the most common path first.
named = mo.group('named') or mo.group('braced')
+ braced = mo.group('braced')
+ if braced is not None:
+ sep = mo.group('sep')
+ if sep:
+ return self.process_braced_group(braced, sep, mapping)
+
if named is not None:
- if ':-' in named:
- var, _, default = named.partition(':-')
- return mapping.get(var) or default
- if '-' in named:
- var, _, default = named.partition('-')
- return mapping.get(var, default)
val = mapping[named]
+ if isinstance(val, six.binary_type):
+ val = val.decode('utf-8')
return '%s' % (val,)
if mo.group('escaped') is not None:
return self.delimiter
@@ -100,3 +153,143 @@ class TemplateWithDefaults(Template):
class InvalidInterpolation(Exception):
def __init__(self, string):
self.string = string
+
+
+class UnsetRequiredSubstitution(Exception):
+ def __init__(self, custom_err_msg):
+ self.err = custom_err_msg
+
+
+PATH_JOKER = '[^.]+'
+FULL_JOKER = '.+'
+
+
+def re_path(*args):
+ return re.compile('^{}$'.format('\.'.join(args)))
+
+
+def re_path_basic(section, name):
+ return re_path(section, PATH_JOKER, name)
+
+
+def service_path(*args):
+ return re_path('service', PATH_JOKER, *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
+ elif s in ['n', 'no', 'false', 'off']:
+ return False
+ raise ValueError('"{}" is not a valid boolean value'.format(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:]
+ try:
+ return int(s, base=0)
+ except ValueError:
+ raise ValueError('"{}" is not a valid integer'.format(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
+
+
+def bytes_to_int(s):
+ v = parse_bytes(s)
+ if v is None:
+ raise ValueError('"{}" is not a valid byte value'.format(s))
+ return v
+
+
+def to_microseconds(v):
+ if not isinstance(v, six.string_types):
+ return v
+ return int(parse_nanoseconds_int(v) / 1000)
+
+
+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('cpu_quota'): to_microseconds,
+ service_path('cpu_period'): to_microseconds,
+ service_path('cpu_rt_period'): to_microseconds,
+ service_path('cpu_rt_runtime'): to_microseconds,
+ 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,
+ service_path('ports', 'published'): to_int,
+ service_path('scale'): to_int,
+ service_path('ulimits', PATH_JOKER): to_int,
+ service_path('ulimits', PATH_JOKER, 'soft'): to_int,
+ service_path('ulimits', PATH_JOKER, 'hard'): to_int,
+ service_path('privileged'): to_boolean,
+ service_path('read_only'): to_boolean,
+ service_path('stdin_open'): to_boolean,
+ service_path('tty'): to_boolean,
+ service_path('volumes', 'read_only'): to_boolean,
+ service_path('volumes', 'volume', 'nocopy'): to_boolean,
+ service_path('volumes', 'tmpfs', 'size'): bytes_to_int,
+ 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):
+ for rexp in self.map.keys():
+ if rexp.match(path):
+ try:
+ return self.map[rexp](value)
+ except ValueError as e:
+ raise ConfigurationError(
+ 'Error while attempting to convert {} to appropriate type: {}'.format(
+ path, e
+ )
+ )
+ return value
+
+
+converter = ConversionMap()
diff --git a/compose/config/serialize.py b/compose/config/serialize.py
index 2b8c73f1..c0cf35c1 100644
--- a/compose/config/serialize.py
+++ b/compose/config/serialize.py
@@ -7,9 +7,11 @@ import yaml
from compose.config import types
from compose.const import COMPOSEFILE_V1 as V1
from compose.const import COMPOSEFILE_V2_1 as V2_1
+from compose.const import COMPOSEFILE_V2_3 as V2_3
from compose.const import COMPOSEFILE_V3_0 as V3_0
from compose.const import COMPOSEFILE_V3_2 as V3_2
from compose.const import COMPOSEFILE_V3_4 as V3_4
+from compose.const import COMPOSEFILE_V3_5 as V3_5
def serialize_config_type(dumper, data):
@@ -25,6 +27,9 @@ def serialize_string(dumper, data):
""" Ensure boolean-like strings are quoted in the output and escape $ characters """
representer = dumper.represent_str if six.PY3 else dumper.represent_unicode
+ if isinstance(data, six.binary_type):
+ data = data.decode('utf-8')
+
data = data.replace('$', '$$')
if data.lower() in ('y', 'n', 'yes', 'no', 'on', 'off', 'true', 'false'):
@@ -34,8 +39,10 @@ def serialize_string(dumper, data):
return representer(data)
+yaml.SafeDumper.add_representer(types.MountSpec, serialize_dict_type)
yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type)
+yaml.SafeDumper.add_representer(types.SecurityOpt, serialize_config_type)
yaml.SafeDumper.add_representer(types.ServiceSecret, serialize_dict_type)
yaml.SafeDumper.add_representer(types.ServiceConfig, serialize_dict_type)
yaml.SafeDumper.add_representer(types.ServicePort, serialize_dict_type)
@@ -67,7 +74,8 @@ def denormalize_config(config, image_digests=None):
del conf['external_name']
if 'name' in conf:
- if config.version < V2_1 or (config.version >= V3_0 and config.version < V3_4):
+ if config.version < V2_1 or (
+ config.version >= V3_0 and config.version < v3_introduced_name_key(key)):
del conf['name']
elif 'external' in conf:
conf['external'] = True
@@ -75,12 +83,19 @@ def denormalize_config(config, image_digests=None):
return result
+def v3_introduced_name_key(key):
+ if key == 'volumes':
+ return V3_4
+ return V3_5
+
+
def serialize_config(config, image_digests=None):
return yaml.safe_dump(
denormalize_config(config, image_digests),
default_flow_style=False,
indent=2,
- width=80
+ width=80,
+ allow_unicode=True
)
@@ -136,10 +151,15 @@ def denormalize_service_dict(service_dict, version, image_digest=None):
service_dict['healthcheck']['start_period'] = serialize_ns_time_value(
service_dict['healthcheck']['start_period']
)
- if 'ports' in service_dict and version < V3_2:
+
+ if 'ports' in service_dict:
service_dict['ports'] = [
- p.legacy_repr() if isinstance(p, types.ServicePort) else p
+ p.legacy_repr() if p.external_ip or version < V3_2 else p
for p in service_dict['ports']
]
+ if 'volumes' in service_dict and (version < V2_3 or (version > V3_0 and version < V3_2)):
+ service_dict['volumes'] = [
+ v.legacy_repr() if isinstance(v, types.MountSpec) else v for v in service_dict['volumes']
+ ]
return service_dict
diff --git a/compose/config/types.py b/compose/config/types.py
index c410343b..ff987521 100644
--- a/compose/config/types.py
+++ b/compose/config/types.py
@@ -4,6 +4,8 @@ Types for objects parsed from the configuration.
from __future__ import absolute_import
from __future__ import unicode_literals
+import json
+import ntpath
import os
import re
from collections import namedtuple
@@ -12,6 +14,7 @@ import six
from docker.utils.ports import build_port_bindings
from ..const import COMPOSEFILE_V1 as V1
+from ..utils import unquote_path
from .errors import ConfigurationError
from compose.const import IS_WINDOWS_PLATFORM
from compose.utils import splitdrive
@@ -133,7 +136,74 @@ def normalize_path_for_engine(path):
return path.replace('\\', '/')
+class MountSpec(object):
+ options_map = {
+ 'volume': {
+ 'nocopy': 'no_copy'
+ },
+ 'bind': {
+ 'propagation': 'propagation'
+ },
+ 'tmpfs': {
+ 'size': 'tmpfs_size'
+ }
+ }
+ _fields = ['type', 'source', 'target', 'read_only', 'consistency']
+
+ @classmethod
+ def parse(cls, mount_dict, normalize=False, win_host=False):
+ normpath = ntpath.normpath if win_host else os.path.normpath
+ if mount_dict.get('source'):
+ if mount_dict['type'] == 'tmpfs':
+ raise ConfigurationError('tmpfs mounts can not specify a source')
+
+ mount_dict['source'] = normpath(mount_dict['source'])
+ if normalize:
+ mount_dict['source'] = normalize_path_for_engine(mount_dict['source'])
+
+ return cls(**mount_dict)
+
+ def __init__(self, type, source=None, target=None, read_only=None, consistency=None, **kwargs):
+ self.type = type
+ self.source = source
+ self.target = target
+ self.read_only = read_only
+ self.consistency = consistency
+ self.options = None
+ if self.type in kwargs:
+ self.options = kwargs[self.type]
+
+ def as_volume_spec(self):
+ mode = 'ro' if self.read_only else 'rw'
+ return VolumeSpec(external=self.source, internal=self.target, mode=mode)
+
+ def legacy_repr(self):
+ return self.as_volume_spec().repr()
+
+ def repr(self):
+ res = {}
+ for field in self._fields:
+ if getattr(self, field, None):
+ res[field] = getattr(self, field)
+ if self.options:
+ res[self.type] = self.options
+ return res
+
+ @property
+ def is_named_volume(self):
+ return self.type == 'volume' and self.source
+
+ @property
+ def is_tmpfs(self):
+ return self.type == 'tmpfs'
+
+ @property
+ def external(self):
+ return self.source
+
+
class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
+ win32 = False
@classmethod
def _parse_unix(cls, volume_config):
@@ -177,7 +247,7 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
else:
external = parts[0]
parts = separate_next_section(parts[1])
- external = os.path.normpath(external)
+ external = ntpath.normpath(external)
internal = parts[0]
if len(parts) > 1:
if ':' in parts[1]:
@@ -190,14 +260,16 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
if normalize:
external = normalize_path_for_engine(external) if external else None
- return cls(external, internal, mode)
+ result = cls(external, internal, mode)
+ result.win32 = True
+ return result
@classmethod
- def parse(cls, volume_config, normalize=False):
+ def parse(cls, volume_config, normalize=False, win_host=False):
"""Parse a volume_config path and split it into external:internal[:mode]
parts to be returned as a valid VolumeSpec.
"""
- if IS_WINDOWS_PLATFORM:
+ if IS_WINDOWS_PLATFORM or win_host:
return cls._parse_win32(volume_config, normalize)
else:
return cls._parse_unix(volume_config)
@@ -210,7 +282,7 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
@property
def is_named_volume(self):
res = self.external and not self.external.startswith(('.', '/', '~'))
- if not IS_WINDOWS_PLATFORM:
+ if not self.win32:
return res
return (
@@ -238,17 +310,18 @@ class ServiceLink(namedtuple('_ServiceLink', 'target alias')):
return self.alias
-class ServiceConfigBase(namedtuple('_ServiceConfigBase', 'source target uid gid mode')):
+class ServiceConfigBase(namedtuple('_ServiceConfigBase', 'source target uid gid mode name')):
@classmethod
def parse(cls, spec):
if isinstance(spec, six.string_types):
- return cls(spec, None, None, None, None)
+ return cls(spec, None, None, None, None, None)
return cls(
spec.get('source'),
spec.get('target'),
spec.get('uid'),
spec.get('gid'),
spec.get('mode'),
+ spec.get('name')
)
@property
@@ -277,11 +350,19 @@ class ServicePort(namedtuple('_ServicePort', 'target published protocol mode ext
except ValueError:
raise ConfigurationError('Invalid target port: {}'.format(target))
- try:
- if published:
- published = int(published)
- except ValueError:
- raise ConfigurationError('Invalid published port: {}'.format(published))
+ if published:
+ if isinstance(published, six.string_types) and '-' in published: # "x-y:z" format
+ a, b = published.split('-', 1)
+ try:
+ int(a)
+ int(b)
+ except ValueError:
+ raise ConfigurationError('Invalid published port: {}'.format(published))
+ else:
+ try:
+ published = int(published)
+ except ValueError:
+ raise ConfigurationError('Invalid published port: {}'.format(published))
return super(ServicePort, cls).__new__(
cls, target, published, *args, **kwargs
@@ -340,6 +421,35 @@ class ServicePort(namedtuple('_ServicePort', 'target published protocol mode ext
return normalize_port_dict(self.repr())
+class GenericResource(namedtuple('_GenericResource', 'kind value')):
+ @classmethod
+ def parse(cls, dct):
+ if 'discrete_resource_spec' not in dct:
+ raise ConfigurationError(
+ 'generic_resource entry must include a discrete_resource_spec key'
+ )
+ if 'kind' not in dct['discrete_resource_spec']:
+ raise ConfigurationError(
+ 'generic_resource entry must include a discrete_resource_spec.kind subkey'
+ )
+ return cls(
+ dct['discrete_resource_spec']['kind'],
+ dct['discrete_resource_spec'].get('value')
+ )
+
+ def repr(self):
+ return {
+ 'discrete_resource_spec': {
+ 'kind': self.kind,
+ 'value': self.value,
+ }
+ }
+
+ @property
+ def merge_field(self):
+ return self.kind
+
+
def normalize_port_dict(port):
return '{external_ip}{has_ext_ip}{published}{is_pub}{target}/{protocol}'.format(
published=port.get('published', ''),
@@ -349,3 +459,36 @@ def normalize_port_dict(port):
external_ip=port.get('external_ip', ''),
has_ext_ip=(':' if port.get('external_ip') else ''),
)
+
+
+class SecurityOpt(namedtuple('_SecurityOpt', 'value src_file')):
+ @classmethod
+ def parse(cls, value):
+ if not isinstance(value, six.string_types):
+ return value
+ # based on https://github.com/docker/cli/blob/9de1b162f/cli/command/container/opts.go#L673-L697
+ con = value.split('=', 2)
+ if len(con) == 1 and con[0] != 'no-new-privileges':
+ if ':' not in value:
+ raise ConfigurationError('Invalid security_opt: {}'.format(value))
+ con = value.split(':', 2)
+
+ if con[0] == 'seccomp' and con[1] != 'unconfined':
+ try:
+ with open(unquote_path(con[1]), 'r') as f:
+ seccomp_data = json.load(f)
+ except (IOError, ValueError) as e:
+ raise ConfigurationError('Error reading seccomp profile: {}'.format(e))
+ return cls(
+ 'seccomp={}'.format(json.dumps(seccomp_data)), con[1]
+ )
+ return cls(value, None)
+
+ def repr(self):
+ if self.src_file is not None:
+ return 'seccomp:{}'.format(self.src_file)
+ return self.value
+
+ @property
+ def merge_field(self):
+ return self.value
diff --git a/compose/config/validation.py b/compose/config/validation.py
index 940775a2..0fdcb37e 100644
--- a/compose/config/validation.py
+++ b/compose/config/validation.py
@@ -44,6 +44,31 @@ DOCKER_CONFIG_HINTS = {
VALID_NAME_CHARS = '[a-zA-Z0-9\._\-]'
VALID_EXPOSE_FORMAT = r'^\d+(\-\d+)?(\/[a-zA-Z]+)?$'
+VALID_IPV4_SEG = r'(\d{1,2}|1\d{2}|2[0-4]\d|25[0-5])'
+VALID_IPV4_ADDR = "({IPV4_SEG}\.){{3}}{IPV4_SEG}".format(IPV4_SEG=VALID_IPV4_SEG)
+VALID_REGEX_IPV4_CIDR = "^{IPV4_ADDR}/(\d|[1-2]\d|3[0-2])$".format(IPV4_ADDR=VALID_IPV4_ADDR)
+
+VALID_IPV6_SEG = r'[0-9a-fA-F]{1,4}'
+VALID_REGEX_IPV6_CIDR = "".join("""
+^
+(
+ (({IPV6_SEG}:){{7}}{IPV6_SEG})|
+ (({IPV6_SEG}:){{1,7}}:)|
+ (({IPV6_SEG}:){{1,6}}(:{IPV6_SEG}){{1,1}})|
+ (({IPV6_SEG}:){{1,5}}(:{IPV6_SEG}){{1,2}})|
+ (({IPV6_SEG}:){{1,4}}(:{IPV6_SEG}){{1,3}})|
+ (({IPV6_SEG}:){{1,3}}(:{IPV6_SEG}){{1,4}})|
+ (({IPV6_SEG}:){{1,2}}(:{IPV6_SEG}){{1,5}})|
+ (({IPV6_SEG}:){{1,1}}(:{IPV6_SEG}){{1,6}})|
+ (:((:{IPV6_SEG}){{1,7}}|:))|
+ (fe80:(:{IPV6_SEG}){{0,4}}%[0-9a-zA-Z]{{1,}})|
+ (::(ffff(:0{{1,4}}){{0,1}}:){{0,1}}{IPV4_ADDR})|
+ (({IPV6_SEG}:){{1,4}}:{IPV4_ADDR})
+)
+/(\d|[1-9]\d|1[0-1]\d|12[0-8])
+$
+""".format(IPV6_SEG=VALID_IPV6_SEG, IPV4_ADDR=VALID_IPV4_ADDR).split())
+
@FormatChecker.cls_checks(format="ports", raises=ValidationError)
def format_ports(instance):
@@ -64,6 +89,16 @@ def format_expose(instance):
return True
+@FormatChecker.cls_checks("subnet_ip_address", raises=ValidationError)
+def format_subnet_ip_address(instance):
+ if isinstance(instance, six.string_types):
+ if not re.match(VALID_REGEX_IPV4_CIDR, instance) and \
+ not re.match(VALID_REGEX_IPV6_CIDR, instance):
+ raise ValidationError("should use the CIDR format")
+
+ return True
+
+
def match_named_volumes(service_dict, project_volumes):
service_volumes = service_dict.get('volumes', [])
for volume_spec in service_volumes:
@@ -391,7 +426,7 @@ def process_config_schema_errors(error):
def validate_against_config_schema(config_file):
schema = load_jsonschema(config_file)
- format_checker = FormatChecker(["ports", "expose"])
+ format_checker = FormatChecker(["ports", "expose", "subnet_ip_address"])
validator = Draft4Validator(
schema,
resolver=RefResolver(get_resolver_path(), schema),
@@ -465,3 +500,27 @@ def handle_errors(errors, format_error_func, filename):
"The Compose file{file_msg} is invalid because:\n{error_msg}".format(
file_msg=" '{}'".format(filename) if filename else "",
error_msg=error_msg))
+
+
+def validate_healthcheck(service_config):
+ healthcheck = service_config.config.get('healthcheck', {})
+
+ if 'test' in healthcheck and isinstance(healthcheck['test'], list):
+ if len(healthcheck['test']) == 0:
+ raise ConfigurationError(
+ 'Service "{}" defines an invalid healthcheck: '
+ '"test" is an empty list'
+ .format(service_config.name))
+
+ # when disable is true config.py::process_healthcheck adds "test: ['NONE']" to service_config
+ elif healthcheck['test'][0] == 'NONE' and len(healthcheck) > 1:
+ raise ConfigurationError(
+ 'Service "{}" defines an invalid healthcheck: '
+ '"disable: true" cannot be combined with other options'
+ .format(service_config.name))
+
+ elif healthcheck['test'][0] not in ('NONE', 'CMD', 'CMD-SHELL'):
+ raise ConfigurationError(
+ 'Service "{}" defines an invalid healthcheck: '
+ 'when "test" is a list the first item must be either NONE, CMD or CMD-SHELL'
+ .format(service_config.name))