summaryrefslogtreecommitdiff
path: root/compose
diff options
context:
space:
mode:
authorRoman Anasal <roman.anasal@bdsu.de>2020-11-15 16:07:54 +0100
committerRoman Anasal <roman.anasal@bdsu.de>2020-12-02 01:08:11 +0100
commit2d2a8a0469cb82dc46554bdc3dfd6663bb0c4ca9 (patch)
tree398231aab0bacfbe6205ceadcd6e0db292b3bc19 /compose
parent5c6c300ba5cfc0226716645d51f400a2d4c19904 (diff)
Implement service profiles
Implement profiles as introduced in compose-spec/compose-spec#110 fixes #7919 closes #1896 closes #6742 closes #7539 Signed-off-by: Roman Anasal <roman.anasal@bdsu.de>
Diffstat (limited to 'compose')
-rw-r--r--compose/cli/command.py18
-rw-r--r--compose/cli/main.py3
-rw-r--r--compose/config/compose_spec.json1
-rw-r--r--compose/config/config.py3
-rw-r--r--compose/project.py56
-rw-r--r--compose/service.py18
6 files changed, 86 insertions, 13 deletions
diff --git a/compose/cli/command.py b/compose/cli/command.py
index d471e78d..ee991309 100644
--- a/compose/cli/command.py
+++ b/compose/cli/command.py
@@ -66,7 +66,8 @@ def project_from_options(project_dir, options, additional_options=None):
environment=environment,
override_dir=override_dir,
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
+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,
context=None, environment=None, override_dir=None,
- interpolate=True, environment_file=None):
+ interpolate=True, environment_file=None, enabled_profiles=None):
if not environment:
environment = Environment.from_env_file(project_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,
environment.get('DOCKER_DEFAULT_PLATFORM'),
execution_context_labels(config_details, environment_file),
+ enabled_profiles,
)
diff --git a/compose/cli/main.py b/compose/cli/main.py
index 6776dae9..21399a17 100644
--- a/compose/cli/main.py
+++ b/compose/cli/main.py
@@ -182,7 +182,7 @@ class TopLevelCommand:
"""Define and run multi-container applications with Docker.
Usage:
- docker-compose [-f <arg>...] [options] [--] [COMMAND] [ARGS...]
+ docker-compose [-f <arg>...] [--profile <name>...] [options] [--] [COMMAND] [ARGS...]
docker-compose -h|--help
Options:
@@ -190,6 +190,7 @@ class TopLevelCommand:
(default: docker-compose.yml)
-p, --project-name NAME Specify an alternate project name
(default: directory name)
+ --profile NAME Specify a profile to enable
-c, --context NAME Specify a context name
--verbose Show more output
--log-level LEVEL Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
diff --git a/compose/config/compose_spec.json b/compose/config/compose_spec.json
index 0ecb3c69..7cc18a17 100644
--- a/compose/config/compose_spec.json
+++ b/compose/config/compose_spec.json
@@ -328,6 +328,7 @@
"uniqueItems": true
},
"privileged": {"type": "boolean"},
+ "profiles": {"$ref": "#/definitions/list_of_strings"},
"pull_policy": {"type": "string", "enum": [
"always", "never", "if_not_present"
]},
diff --git a/compose/config/config.py b/compose/config/config.py
index 1b067e78..eee6b12d 100644
--- a/compose/config/config.py
+++ b/compose/config/config.py
@@ -133,6 +133,7 @@ ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [
'logging',
'network_mode',
'platform',
+ 'profiles',
'scale',
'stop_grace_period',
]
@@ -1047,7 +1048,7 @@ def merge_service_dicts(base, override, version):
for field in [
'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=[])
diff --git a/compose/project.py b/compose/project.py
index 900487d4..49469c1a 100644
--- a/compose/project.py
+++ b/compose/project.py
@@ -68,13 +68,15 @@ class Project:
"""
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.services = services
self.client = client
self.volumes = volumes or ProjectVolumes({})
self.networks = networks or ProjectNetworks({}, False)
self.config_version = config_version
+ self.enabled_profiles = enabled_profiles or []
def labels(self, one_off=OneOffFilter.exclude, legacy=False):
name = self.name
@@ -86,7 +88,8 @@ class Project:
return labels
@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.
"""
@@ -98,7 +101,7 @@ class Project:
networks,
use_networking)
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:
service_dict = dict(service_dict)
@@ -186,7 +189,7 @@ class Project:
if name not in valid_names:
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
by the provided list of names, or all services if service_names is None
@@ -199,15 +202,36 @@ class Project:
reordering as needed to resolve dependencies.
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:
- 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]
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:
- services = reduce(self._inject_deps, services, [])
+ services = reduce(
+ lambda acc, s: self._inject_deps(acc, s, enabled_profiles),
+ services,
+ []
+ )
uniques = []
[uniques.append(s) for s in services if s not in uniques]
@@ -438,10 +462,12 @@ class Project:
self.remove_images(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)
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)
parallel.parallel_execute(
@@ -856,14 +882,26 @@ class Project:
)
)
- def _inject_deps(self, acc, service):
+ def _inject_deps(self, acc, service, enabled_profiles):
dep_names = service.get_dependency_names()
if len(dep_names) > 0:
dep_services = self.get_services(
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:
dep_services = []
diff --git a/compose/service.py b/compose/service.py
index e00a537c..bb55fee5 100644
--- a/compose/service.py
+++ b/compose/service.py
@@ -1331,6 +1331,24 @@ class Service:
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):
aliases = container.get(