mirror of
https://github.com/docker/compose.git
synced 2025-07-22 21:24:38 +02:00
Merge pull request #7930 from acran/profiles
Implement service profiles
This commit is contained in:
commit
5e3708e605
@ -66,7 +66,8 @@ def project_from_options(project_dir, options, additional_options=None):
|
|||||||
environment=environment,
|
environment=environment,
|
||||||
override_dir=override_dir,
|
override_dir=override_dir,
|
||||||
interpolate=(not additional_options.get('--no-interpolate')),
|
interpolate=(not additional_options.get('--no-interpolate')),
|
||||||
environment_file=environment_file
|
environment_file=environment_file,
|
||||||
|
enabled_profiles=get_profiles_from_options(options, environment)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -115,9 +116,21 @@ def get_config_path_from_options(base_dir, options, environment):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_profiles_from_options(options, environment):
|
||||||
|
profile_option = options.get('--profile')
|
||||||
|
if profile_option:
|
||||||
|
return profile_option
|
||||||
|
|
||||||
|
profiles = environment.get('COMPOSE_PROFILE')
|
||||||
|
if profiles:
|
||||||
|
return profiles.split(',')
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
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,
|
||||||
context=None, environment=None, override_dir=None,
|
context=None, environment=None, override_dir=None,
|
||||||
interpolate=True, environment_file=None):
|
interpolate=True, environment_file=None, enabled_profiles=None):
|
||||||
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)
|
||||||
@ -139,6 +152,7 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False,
|
|||||||
client,
|
client,
|
||||||
environment.get('DOCKER_DEFAULT_PLATFORM'),
|
environment.get('DOCKER_DEFAULT_PLATFORM'),
|
||||||
execution_context_labels(config_details, environment_file),
|
execution_context_labels(config_details, environment_file),
|
||||||
|
enabled_profiles,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -182,7 +182,7 @@ class TopLevelCommand:
|
|||||||
"""Define and run multi-container applications with Docker.
|
"""Define and run multi-container applications with Docker.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
docker-compose [-f <arg>...] [options] [--] [COMMAND] [ARGS...]
|
docker-compose [-f <arg>...] [--profile <name>...] [options] [--] [COMMAND] [ARGS...]
|
||||||
docker-compose -h|--help
|
docker-compose -h|--help
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@ -190,6 +190,7 @@ class TopLevelCommand:
|
|||||||
(default: docker-compose.yml)
|
(default: docker-compose.yml)
|
||||||
-p, --project-name NAME Specify an alternate project name
|
-p, --project-name NAME Specify an alternate project name
|
||||||
(default: directory name)
|
(default: directory name)
|
||||||
|
--profile NAME Specify a profile to enable
|
||||||
-c, --context NAME Specify a context name
|
-c, --context NAME Specify a context name
|
||||||
--verbose Show more output
|
--verbose Show more output
|
||||||
--log-level LEVEL Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
--log-level LEVEL Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||||
|
@ -328,6 +328,7 @@
|
|||||||
"uniqueItems": true
|
"uniqueItems": true
|
||||||
},
|
},
|
||||||
"privileged": {"type": "boolean"},
|
"privileged": {"type": "boolean"},
|
||||||
|
"profiles": {"$ref": "#/definitions/list_of_strings"},
|
||||||
"pull_policy": {"type": "string", "enum": [
|
"pull_policy": {"type": "string", "enum": [
|
||||||
"always", "never", "if_not_present"
|
"always", "never", "if_not_present"
|
||||||
]},
|
]},
|
||||||
|
@ -133,6 +133,7 @@ ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [
|
|||||||
'logging',
|
'logging',
|
||||||
'network_mode',
|
'network_mode',
|
||||||
'platform',
|
'platform',
|
||||||
|
'profiles',
|
||||||
'scale',
|
'scale',
|
||||||
'stop_grace_period',
|
'stop_grace_period',
|
||||||
]
|
]
|
||||||
@ -1047,7 +1048,7 @@ def merge_service_dicts(base, override, version):
|
|||||||
|
|
||||||
for field in [
|
for field in [
|
||||||
'cap_add', 'cap_drop', 'expose', 'external_links',
|
'cap_add', 'cap_drop', 'expose', 'external_links',
|
||||||
'volumes_from', 'device_cgroup_rules',
|
'volumes_from', 'device_cgroup_rules', 'profiles',
|
||||||
]:
|
]:
|
||||||
md.merge_field(field, merge_unique_items_lists, default=[])
|
md.merge_field(field, merge_unique_items_lists, default=[])
|
||||||
|
|
||||||
|
@ -68,13 +68,15 @@ class Project:
|
|||||||
"""
|
"""
|
||||||
A collection of services.
|
A collection of services.
|
||||||
"""
|
"""
|
||||||
def __init__(self, name, services, client, networks=None, volumes=None, config_version=None):
|
def __init__(self, name, services, client, networks=None, volumes=None, config_version=None,
|
||||||
|
enabled_profiles=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.services = services
|
self.services = services
|
||||||
self.client = client
|
self.client = client
|
||||||
self.volumes = volumes or ProjectVolumes({})
|
self.volumes = volumes or ProjectVolumes({})
|
||||||
self.networks = networks or ProjectNetworks({}, False)
|
self.networks = networks or ProjectNetworks({}, False)
|
||||||
self.config_version = config_version
|
self.config_version = config_version
|
||||||
|
self.enabled_profiles = enabled_profiles or []
|
||||||
|
|
||||||
def labels(self, one_off=OneOffFilter.exclude, legacy=False):
|
def labels(self, one_off=OneOffFilter.exclude, legacy=False):
|
||||||
name = self.name
|
name = self.name
|
||||||
@ -86,7 +88,8 @@ class Project:
|
|||||||
return labels
|
return labels
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_config(cls, name, config_data, client, default_platform=None, extra_labels=None):
|
def from_config(cls, name, config_data, client, default_platform=None, extra_labels=None,
|
||||||
|
enabled_profiles=None):
|
||||||
"""
|
"""
|
||||||
Construct a Project from a config.Config object.
|
Construct a Project from a config.Config object.
|
||||||
"""
|
"""
|
||||||
@ -98,7 +101,7 @@ class Project:
|
|||||||
networks,
|
networks,
|
||||||
use_networking)
|
use_networking)
|
||||||
volumes = ProjectVolumes.from_config(name, config_data, client)
|
volumes = ProjectVolumes.from_config(name, config_data, client)
|
||||||
project = cls(name, [], client, project_networks, volumes, config_data.version)
|
project = cls(name, [], client, project_networks, volumes, config_data.version, enabled_profiles)
|
||||||
|
|
||||||
for service_dict in config_data.services:
|
for service_dict in config_data.services:
|
||||||
service_dict = dict(service_dict)
|
service_dict = dict(service_dict)
|
||||||
@ -185,7 +188,7 @@ class Project:
|
|||||||
if name not in valid_names:
|
if name not in valid_names:
|
||||||
raise NoSuchService(name)
|
raise NoSuchService(name)
|
||||||
|
|
||||||
def get_services(self, service_names=None, include_deps=False):
|
def get_services(self, service_names=None, include_deps=False, auto_enable_profiles=True):
|
||||||
"""
|
"""
|
||||||
Returns a list of this project's services filtered
|
Returns a list of this project's services filtered
|
||||||
by the provided list of names, or all services if service_names is None
|
by the provided list of names, or all services if service_names is None
|
||||||
@ -198,15 +201,36 @@ class Project:
|
|||||||
reordering as needed to resolve dependencies.
|
reordering as needed to resolve dependencies.
|
||||||
|
|
||||||
Raises NoSuchService if any of the named services do not exist.
|
Raises NoSuchService if any of the named services do not exist.
|
||||||
|
|
||||||
|
Raises ConfigurationError if any service depended on is not enabled by active profiles
|
||||||
"""
|
"""
|
||||||
|
# create a copy so we can *locally* add auto-enabled profiles later
|
||||||
|
enabled_profiles = self.enabled_profiles.copy()
|
||||||
|
|
||||||
if service_names is None or len(service_names) == 0:
|
if service_names is None or len(service_names) == 0:
|
||||||
service_names = self.service_names
|
auto_enable_profiles = False
|
||||||
|
service_names = [
|
||||||
|
service.name
|
||||||
|
for service in self.services
|
||||||
|
if service.enabled_for_profiles(enabled_profiles)
|
||||||
|
]
|
||||||
|
|
||||||
unsorted = [self.get_service(name) for name in service_names]
|
unsorted = [self.get_service(name) for name in service_names]
|
||||||
services = [s for s in self.services if s in unsorted]
|
services = [s for s in self.services if s in unsorted]
|
||||||
|
|
||||||
|
if auto_enable_profiles:
|
||||||
|
# enable profiles of explicitly targeted services
|
||||||
|
for service in services:
|
||||||
|
for profile in service.get_profiles():
|
||||||
|
if profile not in enabled_profiles:
|
||||||
|
enabled_profiles.append(profile)
|
||||||
|
|
||||||
if include_deps:
|
if include_deps:
|
||||||
services = reduce(self._inject_deps, services, [])
|
services = reduce(
|
||||||
|
lambda acc, s: self._inject_deps(acc, s, enabled_profiles),
|
||||||
|
services,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
uniques = []
|
uniques = []
|
||||||
[uniques.append(s) for s in services if s not in uniques]
|
[uniques.append(s) for s in services if s not in uniques]
|
||||||
@ -437,10 +461,12 @@ class Project:
|
|||||||
self.remove_images(remove_image_type)
|
self.remove_images(remove_image_type)
|
||||||
|
|
||||||
def remove_images(self, remove_image_type):
|
def remove_images(self, remove_image_type):
|
||||||
for service in self.get_services():
|
for service in self.services:
|
||||||
service.remove_image(remove_image_type)
|
service.remove_image(remove_image_type)
|
||||||
|
|
||||||
def restart(self, service_names=None, **options):
|
def restart(self, service_names=None, **options):
|
||||||
|
# filter service_names by enabled profiles
|
||||||
|
service_names = [s.name for s in self.get_services(service_names)]
|
||||||
containers = self.containers(service_names, stopped=True)
|
containers = self.containers(service_names, stopped=True)
|
||||||
|
|
||||||
parallel.parallel_execute(
|
parallel.parallel_execute(
|
||||||
@ -855,14 +881,26 @@ class Project:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _inject_deps(self, acc, service):
|
def _inject_deps(self, acc, service, enabled_profiles):
|
||||||
dep_names = service.get_dependency_names()
|
dep_names = service.get_dependency_names()
|
||||||
|
|
||||||
if len(dep_names) > 0:
|
if len(dep_names) > 0:
|
||||||
dep_services = self.get_services(
|
dep_services = self.get_services(
|
||||||
service_names=list(set(dep_names)),
|
service_names=list(set(dep_names)),
|
||||||
include_deps=True
|
include_deps=True,
|
||||||
|
auto_enable_profiles=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for dep in dep_services:
|
||||||
|
if not dep.enabled_for_profiles(enabled_profiles):
|
||||||
|
raise ConfigurationError(
|
||||||
|
'Service "{dep_name}" was pulled in as a dependency of '
|
||||||
|
'service "{service_name}" but is not enabled by the '
|
||||||
|
'active profiles. '
|
||||||
|
'You may fix this by adding a common profile to '
|
||||||
|
'"{dep_name}" and "{service_name}".'
|
||||||
|
.format(dep_name=dep.name, service_name=service.name)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
dep_services = []
|
dep_services = []
|
||||||
|
|
||||||
|
@ -1329,6 +1329,24 @@ class Service:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def get_profiles(self):
|
||||||
|
if 'profiles' not in self.options:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return self.options.get('profiles')
|
||||||
|
|
||||||
|
def enabled_for_profiles(self, enabled_profiles):
|
||||||
|
# if service has no profiles specified it is always enabled
|
||||||
|
if 'profiles' not in self.options:
|
||||||
|
return True
|
||||||
|
|
||||||
|
service_profiles = self.options.get('profiles')
|
||||||
|
for profile in enabled_profiles:
|
||||||
|
if profile in service_profiles:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def short_id_alias_exists(container, network):
|
def short_id_alias_exists(container, network):
|
||||||
aliases = container.get(
|
aliases = container.get(
|
||||||
|
@ -1719,6 +1719,98 @@ services:
|
|||||||
shareable_mode_container = self.project.get_service('shareable').containers()[0]
|
shareable_mode_container = self.project.get_service('shareable').containers()[0]
|
||||||
assert shareable_mode_container.get('HostConfig.IpcMode') == 'shareable'
|
assert shareable_mode_container.get('HostConfig.IpcMode') == 'shareable'
|
||||||
|
|
||||||
|
def test_profiles_up_with_no_profile(self):
|
||||||
|
self.base_dir = 'tests/fixtures/profiles'
|
||||||
|
self.dispatch(['up'])
|
||||||
|
|
||||||
|
containers = self.project.containers(stopped=True)
|
||||||
|
service_names = [c.service for c in containers]
|
||||||
|
|
||||||
|
assert 'foo' in service_names
|
||||||
|
assert len(containers) == 1
|
||||||
|
|
||||||
|
def test_profiles_up_with_profile(self):
|
||||||
|
self.base_dir = 'tests/fixtures/profiles'
|
||||||
|
self.dispatch(['--profile', 'test', 'up'])
|
||||||
|
|
||||||
|
containers = self.project.containers(stopped=True)
|
||||||
|
service_names = [c.service for c in containers]
|
||||||
|
|
||||||
|
assert 'foo' in service_names
|
||||||
|
assert 'bar' in service_names
|
||||||
|
assert 'baz' in service_names
|
||||||
|
assert len(containers) == 3
|
||||||
|
|
||||||
|
def test_profiles_up_invalid_dependency(self):
|
||||||
|
self.base_dir = 'tests/fixtures/profiles'
|
||||||
|
result = self.dispatch(['--profile', 'debug', 'up'], returncode=1)
|
||||||
|
|
||||||
|
assert ('Service "bar" was pulled in as a dependency of service "zot" '
|
||||||
|
'but is not enabled by the active profiles.') in result.stderr
|
||||||
|
|
||||||
|
def test_profiles_up_with_multiple_profiles(self):
|
||||||
|
self.base_dir = 'tests/fixtures/profiles'
|
||||||
|
self.dispatch(['--profile', 'debug', '--profile', 'test', 'up'])
|
||||||
|
|
||||||
|
containers = self.project.containers(stopped=True)
|
||||||
|
service_names = [c.service for c in containers]
|
||||||
|
|
||||||
|
assert 'foo' in service_names
|
||||||
|
assert 'bar' in service_names
|
||||||
|
assert 'baz' in service_names
|
||||||
|
assert 'zot' in service_names
|
||||||
|
assert len(containers) == 4
|
||||||
|
|
||||||
|
def test_profiles_up_with_profile_enabled_by_service(self):
|
||||||
|
self.base_dir = 'tests/fixtures/profiles'
|
||||||
|
self.dispatch(['up', 'bar'])
|
||||||
|
|
||||||
|
containers = self.project.containers(stopped=True)
|
||||||
|
service_names = [c.service for c in containers]
|
||||||
|
|
||||||
|
assert 'bar' in service_names
|
||||||
|
assert len(containers) == 1
|
||||||
|
|
||||||
|
def test_profiles_up_with_dependency_and_profile_enabled_by_service(self):
|
||||||
|
self.base_dir = 'tests/fixtures/profiles'
|
||||||
|
self.dispatch(['up', 'baz'])
|
||||||
|
|
||||||
|
containers = self.project.containers(stopped=True)
|
||||||
|
service_names = [c.service for c in containers]
|
||||||
|
|
||||||
|
assert 'bar' in service_names
|
||||||
|
assert 'baz' in service_names
|
||||||
|
assert len(containers) == 2
|
||||||
|
|
||||||
|
def test_profiles_up_with_invalid_dependency_for_target_service(self):
|
||||||
|
self.base_dir = 'tests/fixtures/profiles'
|
||||||
|
result = self.dispatch(['up', 'zot'], returncode=1)
|
||||||
|
|
||||||
|
assert ('Service "bar" was pulled in as a dependency of service "zot" '
|
||||||
|
'but is not enabled by the active profiles.') in result.stderr
|
||||||
|
|
||||||
|
def test_profiles_up_with_profile_for_dependency(self):
|
||||||
|
self.base_dir = 'tests/fixtures/profiles'
|
||||||
|
self.dispatch(['--profile', 'test', 'up', 'zot'])
|
||||||
|
|
||||||
|
containers = self.project.containers(stopped=True)
|
||||||
|
service_names = [c.service for c in containers]
|
||||||
|
|
||||||
|
assert 'bar' in service_names
|
||||||
|
assert 'zot' in service_names
|
||||||
|
assert len(containers) == 2
|
||||||
|
|
||||||
|
def test_profiles_up_with_merged_profiles(self):
|
||||||
|
self.base_dir = 'tests/fixtures/profiles'
|
||||||
|
self.dispatch(['-f', 'docker-compose.yml', '-f', 'merge-profiles.yml', 'up', 'zot'])
|
||||||
|
|
||||||
|
containers = self.project.containers(stopped=True)
|
||||||
|
service_names = [c.service for c in containers]
|
||||||
|
|
||||||
|
assert 'bar' in service_names
|
||||||
|
assert 'zot' in service_names
|
||||||
|
assert len(containers) == 2
|
||||||
|
|
||||||
def test_exec_without_tty(self):
|
def test_exec_without_tty(self):
|
||||||
self.base_dir = 'tests/fixtures/links-composefile'
|
self.base_dir = 'tests/fixtures/links-composefile'
|
||||||
self.dispatch(['up', '-d', 'console'])
|
self.dispatch(['up', '-d', 'console'])
|
||||||
|
20
tests/fixtures/profiles/docker-compose.yml
vendored
Normal file
20
tests/fixtures/profiles/docker-compose.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
foo:
|
||||||
|
image: busybox:1.31.0-uclibc
|
||||||
|
bar:
|
||||||
|
image: busybox:1.31.0-uclibc
|
||||||
|
profiles:
|
||||||
|
- test
|
||||||
|
baz:
|
||||||
|
image: busybox:1.31.0-uclibc
|
||||||
|
depends_on:
|
||||||
|
- bar
|
||||||
|
profiles:
|
||||||
|
- test
|
||||||
|
zot:
|
||||||
|
image: busybox:1.31.0-uclibc
|
||||||
|
depends_on:
|
||||||
|
- bar
|
||||||
|
profiles:
|
||||||
|
- debug
|
5
tests/fixtures/profiles/merge-profiles.yml
vendored
Normal file
5
tests/fixtures/profiles/merge-profiles.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
bar:
|
||||||
|
profiles:
|
||||||
|
- debug
|
Loading…
x
Reference in New Issue
Block a user