diff options
author | Roman Anasal <roman.anasal@bdsu.de> | 2020-11-15 16:07:54 +0100 |
---|---|---|
committer | Roman Anasal <roman.anasal@bdsu.de> | 2020-12-02 01:08:11 +0100 |
commit | 2d2a8a0469cb82dc46554bdc3dfd6663bb0c4ca9 (patch) | |
tree | 398231aab0bacfbe6205ceadcd6e0db292b3bc19 /compose | |
parent | 5c6c300ba5cfc0226716645d51f400a2d4c19904 (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.py | 18 | ||||
-rw-r--r-- | compose/cli/main.py | 3 | ||||
-rw-r--r-- | compose/config/compose_spec.json | 1 | ||||
-rw-r--r-- | compose/config/config.py | 3 | ||||
-rw-r--r-- | compose/project.py | 56 | ||||
-rw-r--r-- | compose/service.py | 18 |
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( |