Implement compatibility mode,

translating deploy keys to equivalent v2 config if available

Enabled using `--compatibility` CLI flag

Signed-off-by: Joffrey F <joffrey@docker.com>
This commit is contained in:
Joffrey F 2018-02-05 17:31:17 -08:00
parent d2c87f7649
commit 8c297f267e
4 changed files with 101 additions and 19 deletions

View File

@ -38,6 +38,7 @@ def project_from_options(project_dir, options):
tls_config=tls_config_from_options(options, environment), tls_config=tls_config_from_options(options, environment),
environment=environment, environment=environment,
override_dir=options.get('--project-directory'), override_dir=options.get('--project-directory'),
compatibility=options.get('--compatibility'),
) )
@ -63,7 +64,8 @@ def get_config_from_options(base_dir, options):
base_dir, options, environment base_dir, options, environment
) )
return config.load( return config.load(
config.find(base_dir, config_path, environment) config.find(base_dir, config_path, environment),
options.get('--compatibility')
) )
@ -100,14 +102,15 @@ def get_client(environment, verbose=False, version=None, tls_config=None, host=N
def get_project(project_dir, config_path=None, project_name=None, verbose=False, def get_project(project_dir, config_path=None, project_name=None, verbose=False,
host=None, tls_config=None, environment=None, override_dir=None): host=None, tls_config=None, environment=None, override_dir=None,
compatibility=False):
if not environment: if not environment:
environment = Environment.from_env_file(project_dir) environment = Environment.from_env_file(project_dir)
config_details = config.find(project_dir, config_path, environment, override_dir) config_details = config.find(project_dir, config_path, environment, override_dir)
project_name = get_project_name( project_name = get_project_name(
config_details.working_dir, project_name, environment config_details.working_dir, project_name, environment
) )
config_data = config.load(config_details) config_data = config.load(config_details, compatibility)
api_version = environment.get( api_version = environment.get(
'COMPOSE_API_VERSION', 'COMPOSE_API_VERSION',

View File

@ -185,6 +185,8 @@ class TopLevelCommand(object):
is an IP address) is an IP address)
--project-directory PATH Specify an alternate working directory --project-directory PATH Specify an alternate working directory
(default: the path of the Compose file) (default: the path of the Compose file)
--compatibility If set, Compose will attempt to convert deploy keys in v3
files to their non-Swarm equivalent
Commands: Commands:
build Build or rebuild services build Build or rebuild services

View File

@ -16,6 +16,7 @@ from . import types
from .. import const from .. import const
from ..const import COMPOSEFILE_V1 as V1 from ..const import COMPOSEFILE_V1 as V1
from ..const import COMPOSEFILE_V2_1 as V2_1 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_0 as V3_0
from ..const import COMPOSEFILE_V3_4 as V3_4 from ..const import COMPOSEFILE_V3_4 as V3_4
from ..utils import build_string_dict from ..utils import build_string_dict
@ -341,7 +342,7 @@ def find_candidates_in_parent_dirs(filenames, path):
return (candidates, path) return (candidates, path)
def check_swarm_only_config(service_dicts): def check_swarm_only_config(service_dicts, compatibility=False):
warning_template = ( warning_template = (
"Some services ({services}) use the '{key}' key, which will be ignored. " "Some services ({services}) use the '{key}' key, which will be ignored. "
"Compose does not support '{key}' configuration - use " "Compose does not support '{key}' configuration - use "
@ -357,13 +358,13 @@ def check_swarm_only_config(service_dicts):
key=key key=key
) )
) )
if not compatibility:
check_swarm_only_key(service_dicts, 'deploy') check_swarm_only_key(service_dicts, 'deploy')
check_swarm_only_key(service_dicts, 'credential_spec') check_swarm_only_key(service_dicts, 'credential_spec')
check_swarm_only_key(service_dicts, 'configs') 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 """Load the configuration from a working directory and a list of
configuration files. Files are loaded in order, and merged on top configuration files. Files are loaded in order, and merged on top
of each other to create the final configuration. of each other to create the final configuration.
@ -391,15 +392,17 @@ def load(config_details):
configs = load_mapping( configs = load_mapping(
config_details.config_files, 'get_configs', 'Config', config_details.working_dir 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: if main_file.version != V1:
for service_dict in service_dicts: for service_dict in service_dicts:
match_named_volumes(service_dict, volumes) match_named_volumes(service_dict, volumes)
check_swarm_only_config(service_dicts) check_swarm_only_config(service_dicts, compatibility)
return Config(main_file.version, service_dicts, volumes, networks, secrets, configs) version = V2_3 if compatibility and main_file.version >= V3_0 else main_file.version
return Config(version, service_dicts, volumes, networks, secrets, configs)
def load_mapping(config_files, get_func, entity_type, working_dir=None): def load_mapping(config_files, get_func, entity_type, working_dir=None):
@ -441,7 +444,7 @@ def validate_external(entity_type, name, config, version):
entity_type, name, ', '.join(k for k in config if k != 'external'))) 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): def build_service(service_name, service_dict, service_names):
service_config = ServiceConfig.with_abs_paths( service_config = ServiceConfig.with_abs_paths(
config_details.working_dir, config_details.working_dir,
@ -459,7 +462,9 @@ def load_services(config_details, config_file):
service_config, service_config,
service_names, service_names,
config_file.version, config_file.version,
config_details.environment) config_details.environment,
compatibility
)
return service_dict return service_dict
def build_services(service_config): def build_services(service_config):
@ -827,7 +832,7 @@ def finalize_service_volumes(service_dict, environment):
return service_dict return service_dict
def finalize_service(service_config, service_names, version, environment): def finalize_service(service_config, service_names, version, environment, compatibility):
service_dict = dict(service_config.config) service_dict = dict(service_config.config)
if 'environment' in service_dict or 'env_file' in service_dict: if 'environment' in service_dict or 'env_file' in service_dict:
@ -868,10 +873,79 @@ def finalize_service(service_config, service_names, version, environment):
normalize_build(service_dict, service_config.working_dir, 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 service_dict['name'] = service_config.name
return normalize_v1_service_format(service_dict) 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']
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':
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): def normalize_v1_service_format(service_dict):
if 'log_driver' in service_dict or 'log_opt' in service_dict: if 'log_driver' in service_dict or 'log_opt' in service_dict:
if 'logging' not in service_dict: if 'logging' not in service_dict:

View File

@ -1,8 +1,7 @@
version: "3.2" version: "3.5"
services: services:
web: web:
image: busybox image: busybox
deploy: deploy:
mode: replicated mode: replicated
replicas: 6 replicas: 6
@ -15,18 +14,22 @@ services:
max_failure_ratio: 0.3 max_failure_ratio: 0.3
resources: resources:
limits: limits:
cpus: '0.001' cpus: '0.05'
memory: 50M memory: 50M
reservations: reservations:
cpus: '0.0001' cpus: '0.01'
memory: 20M memory: 20M
restart_policy: restart_policy:
condition: on_failure condition: on-failure
delay: 5s delay: 5s
max_attempts: 3 max_attempts: 3
window: 120s window: 120s
placement: placement:
constraints: [node=foo] constraints:
- node.hostname==foo
- node.role != manager
preferences:
- spread: node.labels.datacenter
healthcheck: healthcheck:
test: cat /etc/passwd test: cat /etc/passwd