Implement service mode for ipc

Signed-off-by: Eric Hripko <ehripko@bloomberg.net>
This commit is contained in:
Eric Hripko 2020-05-01 16:56:46 +01:00
parent d4f55a721d
commit efb5601323
8 changed files with 146 additions and 1 deletions

View File

@ -47,6 +47,7 @@ from .validation import validate_credential_spec
from .validation import validate_depends_on from .validation import validate_depends_on
from .validation import validate_extends_file_path from .validation import validate_extends_file_path
from .validation import validate_healthcheck from .validation import validate_healthcheck
from .validation import validate_ipc_mode
from .validation import validate_links from .validation import validate_links
from .validation import validate_network_mode from .validation import validate_network_mode
from .validation import validate_pid_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_cpu(service_config)
validate_ulimits(service_config) validate_ulimits(service_config)
validate_ipc_mode(service_config, service_names)
validate_network_mode(service_config, service_names) validate_network_mode(service_config, service_names)
validate_pid_mode(service_config, service_names) validate_pid_mode(service_config, service_names)
validate_depends_on(service_config, service_names) validate_depends_on(service_config, service_names)

View File

@ -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 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('network_mode')) or
name == get_service_name_from_network_mode(service.get('pid')) 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', [])) name in service.get('depends_on', []))
] ]

View File

@ -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): def validate_links(service_config, service_names):
for link in service_config.config.get('links', []): for link in service_config.config.get('links', []):
if link.split(':')[0] not in service_names: if link.split(':')[0] not in service_names:

View File

@ -26,14 +26,17 @@ from .network import get_networks
from .network import ProjectNetworks from .network import ProjectNetworks
from .progress_stream import read_status from .progress_stream import read_status
from .service import BuildAction from .service import BuildAction
from .service import ContainerIpcMode
from .service import ContainerNetworkMode from .service import ContainerNetworkMode
from .service import ContainerPidMode from .service import ContainerPidMode
from .service import ConvergenceStrategy from .service import ConvergenceStrategy
from .service import IpcMode
from .service import NetworkMode from .service import NetworkMode
from .service import NoSuchImageError from .service import NoSuchImageError
from .service import parse_repository_tag from .service import parse_repository_tag
from .service import PidMode from .service import PidMode
from .service import Service from .service import Service
from .service import ServiceIpcMode
from .service import ServiceNetworkMode from .service import ServiceNetworkMode
from .service import ServicePidMode from .service import ServicePidMode
from .utils import microseconds_from_time_nano from .utils import microseconds_from_time_nano
@ -106,6 +109,7 @@ class Project(object):
service_dict.pop('networks', None) service_dict.pop('networks', None)
links = project.get_links(service_dict) links = project.get_links(service_dict)
ipc_mode = project.get_ipc_mode(service_dict)
network_mode = project.get_network_mode( network_mode = project.get_network_mode(
service_dict, list(service_networks.keys()) service_dict, list(service_networks.keys())
) )
@ -147,6 +151,7 @@ class Project(object):
volumes_from=volumes_from, volumes_from=volumes_from,
secrets=secrets, secrets=secrets,
pid_mode=pid_mode, pid_mode=pid_mode,
ipc_mode=ipc_mode,
platform=service_dict.pop('platform', None), platform=service_dict.pop('platform', None),
default_platform=default_platform, default_platform=default_platform,
extra_labels=extra_labels, extra_labels=extra_labels,
@ -274,6 +279,27 @@ class Project(object):
return PidMode(pid_mode) 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): def get_service_scale(self, service_dict):
# service.scale for v2 and deploy.replicas for v3 # service.scale for v2 and deploy.replicas for v3
scale = service_dict.get('scale', None) scale = service_dict.get('scale', None)

View File

@ -176,6 +176,7 @@ class Service(object):
networks=None, networks=None,
secrets=None, secrets=None,
scale=1, scale=1,
ipc_mode=None,
pid_mode=None, pid_mode=None,
default_platform=None, default_platform=None,
extra_labels=None, extra_labels=None,
@ -187,6 +188,7 @@ class Service(object):
self.use_networking = use_networking self.use_networking = use_networking
self.links = links or [] self.links = links or []
self.volumes_from = volumes_from or [] self.volumes_from = volumes_from or []
self.ipc_mode = ipc_mode or IpcMode(None)
self.network_mode = network_mode or NetworkMode(None) self.network_mode = network_mode or NetworkMode(None)
self.pid_mode = pid_mode or PidMode(None) self.pid_mode = pid_mode or PidMode(None)
self.networks = networks or {} self.networks = networks or {}
@ -719,17 +721,20 @@ class Service(object):
def get_dependency_names(self): def get_dependency_names(self):
net_name = self.network_mode.service_name net_name = self.network_mode.service_name
pid_namespace = self.pid_mode.service_name pid_namespace = self.pid_mode.service_name
ipc_namespace = self.ipc_mode.service_name
return ( return (
self.get_linked_service_names() + self.get_linked_service_names() +
self.get_volumes_from_names() + self.get_volumes_from_names() +
([net_name] if net_name else []) + ([net_name] if net_name else []) +
([pid_namespace] if pid_namespace else []) + ([pid_namespace] if pid_namespace else []) +
([ipc_namespace] if ipc_namespace else []) +
list(self.options.get('depends_on', {}).keys()) list(self.options.get('depends_on', {}).keys())
) )
def get_dependency_configs(self): def get_dependency_configs(self):
net_name = self.network_mode.service_name net_name = self.network_mode.service_name
pid_namespace = self.pid_mode.service_name pid_namespace = self.pid_mode.service_name
ipc_namespace = self.ipc_mode.service_name
configs = dict( configs = dict(
[(name, None) for name in self.get_linked_service_names()] [(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({net_name: None} if net_name else {})
configs.update({pid_namespace: None} if pid_namespace 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', {})) configs.update(self.options.get('depends_on', {}))
for svc, config in self.options.get('depends_on', {}).items(): for svc, config in self.options.get('depends_on', {}).items():
if config['condition'] == CONDITION_STARTED: if config['condition'] == CONDITION_STARTED:
@ -1025,7 +1031,7 @@ class Service(object):
read_only=options.get('read_only'), read_only=options.get('read_only'),
pid_mode=self.pid_mode.mode, pid_mode=self.pid_mode.mode,
security_opt=security_opt, security_opt=security_opt,
ipc_mode=options.get('ipc'), ipc_mode=self.ipc_mode.mode,
cgroup_parent=options.get('cgroup_parent'), cgroup_parent=options.get('cgroup_parent'),
cpu_quota=options.get('cpu_quota'), cpu_quota=options.get('cpu_quota'),
shm_size=options.get('shm_size'), shm_size=options.get('shm_size'),
@ -1329,6 +1335,46 @@ def short_id_alias_exists(container, network):
return container.short_id in aliases 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): class PidMode(object):
def __init__(self, mode): def __init__(self, mode):
self._mode = mode self._mode = mode

View File

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

View File

@ -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

View File

@ -38,6 +38,7 @@ from compose.project import Project
from compose.service import BuildAction from compose.service import BuildAction
from compose.service import ConvergencePlan from compose.service import ConvergencePlan
from compose.service import ConvergenceStrategy from compose.service import ConvergenceStrategy
from compose.service import IpcMode
from compose.service import NetworkMode from compose.service import NetworkMode
from compose.service import PidMode from compose.service import PidMode
from compose.service import Service from compose.service import Service
@ -1480,6 +1481,17 @@ class ServiceTest(DockerClientTestCase):
container = create_and_start_container(service) container = create_and_start_container(service)
assert container.get('HostConfig.PidMode') == 'host' 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() @v2_1_only()
def test_userns_mode_none_defined(self): def test_userns_mode_none_defined(self):
service = self.create_service('web', userns_mode=None) service = self.create_service('web', userns_mode=None)