From efb5601323d243595248f85e2e2aa57cb0afded9 Mon Sep 17 00:00:00 2001 From: Eric Hripko Date: Fri, 1 May 2020 16:56:46 +0100 Subject: [PATCH] Implement service mode for ipc Signed-off-by: Eric Hripko --- compose/config/config.py | 2 + compose/config/sort_services.py | 1 + compose/config/validation.py | 15 +++++++ compose/project.py | 26 ++++++++++++ compose/service.py | 48 +++++++++++++++++++++- tests/acceptance/cli_test.py | 26 ++++++++++++ tests/fixtures/ipc-mode/docker-compose.yml | 17 ++++++++ tests/integration/service_test.py | 12 ++++++ 8 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/ipc-mode/docker-compose.yml diff --git a/compose/config/config.py b/compose/config/config.py index c5791e153..90baeeaa8 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -47,6 +47,7 @@ from .validation import validate_credential_spec from .validation import validate_depends_on from .validation import validate_extends_file_path from .validation import validate_healthcheck +from .validation import validate_ipc_mode from .validation import validate_links from .validation import validate_network_mode from .validation import validate_pid_mode @@ -734,6 +735,7 @@ def validate_service(service_config, service_names, config_file): validate_cpu(service_config) validate_ulimits(service_config) + validate_ipc_mode(service_config, service_names) validate_network_mode(service_config, service_names) validate_pid_mode(service_config, service_names) validate_depends_on(service_config, service_names) diff --git a/compose/config/sort_services.py b/compose/config/sort_services.py index a600139b2..65953891f 100644 --- a/compose/config/sort_services.py +++ b/compose/config/sort_services.py @@ -36,6 +36,7 @@ def get_service_dependents(service_dict, services): name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or name == get_service_name_from_network_mode(service.get('network_mode')) or name == get_service_name_from_network_mode(service.get('pid')) or + name == get_service_name_from_network_mode(service.get('ipc')) or name in service.get('depends_on', [])) ] diff --git a/compose/config/validation.py b/compose/config/validation.py index 942c4e036..61a3370db 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -218,6 +218,21 @@ def validate_pid_mode(service_config, service_names): ) +def validate_ipc_mode(service_config, service_names): + ipc_mode = service_config.config.get('ipc') + if not ipc_mode: + return + + dependency = get_service_name_from_network_mode(ipc_mode) + if not dependency: + return + if dependency not in service_names: + raise ConfigurationError( + "Service '{s.name}' uses the IPC namespace of service '{dep}' which " + "is undefined.".format(s=service_config, dep=dependency) + ) + + def validate_links(service_config, service_names): for link in service_config.config.get('links', []): if link.split(':')[0] not in service_names: diff --git a/compose/project.py b/compose/project.py index e80d68ef1..5a03b30ca 100644 --- a/compose/project.py +++ b/compose/project.py @@ -26,14 +26,17 @@ from .network import get_networks from .network import ProjectNetworks from .progress_stream import read_status from .service import BuildAction +from .service import ContainerIpcMode from .service import ContainerNetworkMode from .service import ContainerPidMode from .service import ConvergenceStrategy +from .service import IpcMode from .service import NetworkMode from .service import NoSuchImageError from .service import parse_repository_tag from .service import PidMode from .service import Service +from .service import ServiceIpcMode from .service import ServiceNetworkMode from .service import ServicePidMode from .utils import microseconds_from_time_nano @@ -106,6 +109,7 @@ class Project(object): service_dict.pop('networks', None) links = project.get_links(service_dict) + ipc_mode = project.get_ipc_mode(service_dict) network_mode = project.get_network_mode( service_dict, list(service_networks.keys()) ) @@ -147,6 +151,7 @@ class Project(object): volumes_from=volumes_from, secrets=secrets, pid_mode=pid_mode, + ipc_mode=ipc_mode, platform=service_dict.pop('platform', None), default_platform=default_platform, extra_labels=extra_labels, @@ -274,6 +279,27 @@ class Project(object): return PidMode(pid_mode) + def get_ipc_mode(self, service_dict): + ipc_mode = service_dict.pop('ipc', None) + if not ipc_mode: + return IpcMode(None) + + service_name = get_service_name_from_network_mode(ipc_mode) + if service_name: + return ServiceIpcMode(self.get_service(service_name)) + + container_name = get_container_name_from_network_mode(ipc_mode) + if container_name: + try: + return ContainerIpcMode(Container.from_id(self.client, container_name)) + except APIError: + raise ConfigurationError( + "Service '{name}' uses the IPC namespace of container '{dep}' which " + "does not exist.".format(name=service_dict['name'], dep=container_name) + ) + + return IpcMode(ipc_mode) + def get_service_scale(self, service_dict): # service.scale for v2 and deploy.replicas for v3 scale = service_dict.get('scale', None) diff --git a/compose/service.py b/compose/service.py index 50b002796..f52bd6ffe 100644 --- a/compose/service.py +++ b/compose/service.py @@ -176,6 +176,7 @@ class Service(object): networks=None, secrets=None, scale=1, + ipc_mode=None, pid_mode=None, default_platform=None, extra_labels=None, @@ -187,6 +188,7 @@ class Service(object): self.use_networking = use_networking self.links = links or [] self.volumes_from = volumes_from or [] + self.ipc_mode = ipc_mode or IpcMode(None) self.network_mode = network_mode or NetworkMode(None) self.pid_mode = pid_mode or PidMode(None) self.networks = networks or {} @@ -719,17 +721,20 @@ class Service(object): def get_dependency_names(self): net_name = self.network_mode.service_name pid_namespace = self.pid_mode.service_name + ipc_namespace = self.ipc_mode.service_name return ( self.get_linked_service_names() + self.get_volumes_from_names() + ([net_name] if net_name else []) + ([pid_namespace] if pid_namespace else []) + + ([ipc_namespace] if ipc_namespace else []) + list(self.options.get('depends_on', {}).keys()) ) def get_dependency_configs(self): net_name = self.network_mode.service_name pid_namespace = self.pid_mode.service_name + ipc_namespace = self.ipc_mode.service_name configs = dict( [(name, None) for name in self.get_linked_service_names()] @@ -739,6 +744,7 @@ class Service(object): )) configs.update({net_name: None} if net_name else {}) configs.update({pid_namespace: None} if pid_namespace else {}) + configs.update({ipc_namespace: None} if ipc_namespace else {}) configs.update(self.options.get('depends_on', {})) for svc, config in self.options.get('depends_on', {}).items(): if config['condition'] == CONDITION_STARTED: @@ -1025,7 +1031,7 @@ class Service(object): read_only=options.get('read_only'), pid_mode=self.pid_mode.mode, security_opt=security_opt, - ipc_mode=options.get('ipc'), + ipc_mode=self.ipc_mode.mode, cgroup_parent=options.get('cgroup_parent'), cpu_quota=options.get('cpu_quota'), shm_size=options.get('shm_size'), @@ -1329,6 +1335,46 @@ def short_id_alias_exists(container, network): return container.short_id in aliases +class IpcMode(object): + def __init__(self, mode): + self._mode = mode + + @property + def mode(self): + return self._mode + + @property + def service_name(self): + return None + + +class ServiceIpcMode(IpcMode): + def __init__(self, service): + self.service = service + + @property + def service_name(self): + return self.service.name + + @property + def mode(self): + containers = self.service.containers() + if containers: + return 'container:' + containers[0].id + + log.warning( + "Service %s is trying to use reuse the IPC namespace " + "of another service that is not running." % (self.service_name) + ) + return None + + +class ContainerIpcMode(IpcMode): + def __init__(self, container): + self.container = container + self._mode = 'container:{}'.format(container.id) + + class PidMode(object): def __init__(self, mode): self._mode = mode diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 825586514..7fae1f22e 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1681,6 +1681,32 @@ services: host_mode_container = self.project.get_service('host').containers()[0] assert host_mode_container.get('HostConfig.PidMode') == 'host' + @v2_only() + @no_cluster('Container IPC mode does not work across clusters') + def test_up_with_ipc_mode(self): + c = self.client.create_container( + 'busybox', 'top', name='composetest_ipc_mode_container', + host_config={} + ) + self.addCleanup(self.client.remove_container, c, force=True) + self.client.start(c) + container_mode_source = 'container:{}'.format(c['Id']) + + self.base_dir = 'tests/fixtures/ipc-mode' + + self.dispatch(['up', '-d'], None) + + service_mode_source = 'container:{}'.format( + self.project.get_service('shareable').containers()[0].id) + service_mode_container = self.project.get_service('service').containers()[0] + assert service_mode_container.get('HostConfig.IpcMode') == service_mode_source + + container_mode_container = self.project.get_service('container').containers()[0] + assert container_mode_container.get('HostConfig.IpcMode') == container_mode_source + + shareable_mode_container = self.project.get_service('shareable').containers()[0] + assert shareable_mode_container.get('HostConfig.IpcMode') == 'shareable' + def test_exec_without_tty(self): self.base_dir = 'tests/fixtures/links-composefile' self.dispatch(['up', '-d', 'console']) diff --git a/tests/fixtures/ipc-mode/docker-compose.yml b/tests/fixtures/ipc-mode/docker-compose.yml new file mode 100644 index 000000000..c58ce2448 --- /dev/null +++ b/tests/fixtures/ipc-mode/docker-compose.yml @@ -0,0 +1,17 @@ +version: "2.4" + +services: + service: + image: busybox + command: top + ipc: "service:shareable" + + container: + image: busybox + command: top + ipc: "container:composetest_ipc_mode_container" + + shareable: + image: busybox + command: top + ipc: shareable diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 9830accb8..ba6dd1770 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -38,6 +38,7 @@ from compose.project import Project from compose.service import BuildAction from compose.service import ConvergencePlan from compose.service import ConvergenceStrategy +from compose.service import IpcMode from compose.service import NetworkMode from compose.service import PidMode from compose.service import Service @@ -1480,6 +1481,17 @@ class ServiceTest(DockerClientTestCase): container = create_and_start_container(service) assert container.get('HostConfig.PidMode') == 'host' + def test_ipc_mode_none_defined(self): + service = self.create_service('web', ipc_mode=None) + container = create_and_start_container(service) + print(container.get('HostConfig.IpcMode')) + assert container.get('HostConfig.IpcMode') == 'shareable' + + def test_ipc_mode_host(self): + service = self.create_service('web', ipc_mode=IpcMode('host')) + container = create_and_start_container(service) + assert container.get('HostConfig.IpcMode') == 'host' + @v2_1_only() def test_userns_mode_none_defined(self): service = self.create_service('web', userns_mode=None)