Skip to content

Commit

Permalink
Merge pull request #5684 from docker/compat_mode
Browse files Browse the repository at this point in the history
Compatibility mode
  • Loading branch information
shin- authored Feb 26, 2018
2 parents 6659e99 + 51076b5 commit ec0de7e
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 30 deletions.
9 changes: 6 additions & 3 deletions compose/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def project_from_options(project_dir, options):
tls_config=tls_config_from_options(options, environment),
environment=environment,
override_dir=options.get('--project-directory'),
compatibility=options.get('--compatibility'),
)


Expand All @@ -63,7 +64,8 @@ def get_config_from_options(base_dir, options):
base_dir, options, environment
)
return config.load(
config.find(base_dir, config_path, environment)
config.find(base_dir, config_path, environment),
options.get('--compatibility')
)


Expand Down Expand Up @@ -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,
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:
environment = Environment.from_env_file(project_dir)
config_details = config.find(project_dir, config_path, environment, override_dir)
project_name = get_project_name(
config_details.working_dir, project_name, environment
)
config_data = config.load(config_details)
config_data = config.load(config_details, compatibility)

api_version = environment.get(
'COMPOSE_API_VERSION',
Expand Down
13 changes: 8 additions & 5 deletions compose/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,10 @@ class TopLevelCommand(object):
docker-compose -h|--help
Options:
-f, --file FILE Specify an alternate compose file (default: docker-compose.yml)
-p, --project-name NAME Specify an alternate project name (default: directory name)
-f, --file FILE Specify an alternate compose file
(default: docker-compose.yml)
-p, --project-name NAME Specify an alternate project name
(default: directory name)
--verbose Show more output
--log-level LEVEL Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
--no-ansi Do not print ANSI control characters
Expand All @@ -199,11 +201,12 @@ class TopLevelCommand(object):
--tlscert CLIENT_CERT_PATH Path to TLS certificate file
--tlskey TLS_KEY_PATH Path to TLS key file
--tlsverify Use TLS and verify the remote
--skip-hostname-check Don't check the daemon's hostname against the name specified
in the client certificate (for example if your docker host
is an IP address)
--skip-hostname-check Don't check the daemon's hostname against the
name specified in the client certificate
--project-directory PATH Specify an alternate working directory
(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:
build Build or rebuild services
Expand Down
95 changes: 85 additions & 10 deletions compose/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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
Expand Down Expand Up @@ -341,7 +342,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 "
Expand All @@ -357,13 +358,13 @@ def check_swarm_only_key(service_dicts, key):
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.
Expand Down Expand Up @@ -391,15 +392,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):
Expand Down Expand Up @@ -441,7 +444,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,
Expand All @@ -459,7 +462,9 @@ def build_service(service_name, service_dict, service_names):
service_config,
service_names,
config_file.version,
config_details.environment)
config_details.environment,
compatibility
)
return service_dict

def build_services(service_config):
Expand Down Expand Up @@ -827,7 +832,7 @@ def finalize_service_volumes(service_dict, environment):
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)

if 'environment' in service_dict or 'env_file' in service_dict:
Expand Down Expand Up @@ -868,10 +873,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:
Expand Down
2 changes: 1 addition & 1 deletion compose/config/config_schema_v3.6.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v3.5.json",
"id": "config_schema_v3.6.json",
"type": "object",
"required": ["version"],

Expand Down
34 changes: 29 additions & 5 deletions tests/acceptance/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ def test_config_v3(self):
result = self.dispatch(['config'])

assert yaml.load(result.stdout) == {
'version': '3.2',
'version': '3.5',
'volumes': {
'foobar': {
'labels': {
Expand All @@ -419,22 +419,25 @@ def test_config_v3(self):
},
'resources': {
'limits': {
'cpus': '0.001',
'cpus': '0.05',
'memory': '50M',
},
'reservations': {
'cpus': '0.0001',
'cpus': '0.01',
'memory': '20M',
},
},
'restart_policy': {
'condition': 'on_failure',
'condition': 'on-failure',
'delay': '5s',
'max_attempts': 3,
'window': '120s',
},
'placement': {
'constraints': ['node=foo'],
'constraints': [
'node.hostname==foo', 'node.role != manager'
],
'preferences': [{'spread': 'node.labels.datacenter'}]
},
},

Expand Down Expand Up @@ -464,6 +467,27 @@ def test_config_v3(self):
},
}

def test_config_compatibility_mode(self):
self.base_dir = 'tests/fixtures/compatibility-mode'
result = self.dispatch(['--compatibility', 'config'])

assert yaml.load(result.stdout) == {
'version': '2.3',
'volumes': {'foo': {'driver': 'default'}},
'services': {
'foo': {
'command': '/bin/true',
'image': 'alpine:3.7',
'scale': 3,
'restart': 'always:7',
'mem_limit': '300M',
'mem_reservation': '100M',
'cpus': 0.7,
'volumes': ['foo:/bar:rw']
}
}
}

def test_ps(self):
self.project.get_service('simple').create_container()
result = self.dispatch(['ps'])
Expand Down
22 changes: 22 additions & 0 deletions tests/fixtures/compatibility-mode/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version: '3.5'
services:
foo:
image: alpine:3.7
command: /bin/true
deploy:
replicas: 3
restart_policy:
condition: any
max_attempts: 7
resources:
limits:
memory: 300M
cpus: '0.7'
reservations:
memory: 100M
volumes:
- foo:/bar

volumes:
foo:
driver: default
15 changes: 9 additions & 6 deletions tests/fixtures/v3-full/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
version: "3.2"
version: "3.5"
services:
web:
image: busybox

deploy:
mode: replicated
replicas: 6
Expand All @@ -15,18 +14,22 @@ services:
max_failure_ratio: 0.3
resources:
limits:
cpus: '0.001'
cpus: '0.05'
memory: 50M
reservations:
cpus: '0.0001'
cpus: '0.01'
memory: 20M
restart_policy:
condition: on_failure
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
placement:
constraints: [node=foo]
constraints:
- node.hostname==foo
- node.role != manager
preferences:
- spread: node.labels.datacenter

healthcheck:
test: cat /etc/passwd
Expand Down
Loading

0 comments on commit ec0de7e

Please sign in to comment.