Просмотр исходного кода

Merge pull request #7417 from EricHripko/ipc-service

[Compose Spec] Implement service mode for ipc
Anca Iordache 5 лет назад
Родитель
Сommit
b23eb2deab

+ 2 - 0
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)

+ 1 - 0
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', []))
     ]
 

+ 15 - 0
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:

+ 26 - 0
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)

+ 47 - 1
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

+ 25 - 0
tests/acceptance/cli_test.py

@@ -1681,6 +1681,31 @@ services:
         host_mode_container = self.project.get_service('host').containers()[0]
         assert host_mode_container.get('HostConfig.PidMode') == 'host'
 
+    @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'])

+ 17 - 0
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

+ 12 - 0
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)