diff --git a/compose/config/config.py b/compose/config/config.py index fdb20df19..fb5442566 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -44,6 +44,7 @@ from .validation import validate_depends_on from .validation import validate_extends_file_path from .validation import validate_links from .validation import validate_network_mode +from .validation import validate_pid_mode from .validation import validate_service_constraints from .validation import validate_top_level_object from .validation import validate_ulimits @@ -667,6 +668,7 @@ def validate_service(service_config, service_names, config_file): validate_cpu(service_config) validate_ulimits(service_config) validate_network_mode(service_config, service_names) + validate_pid_mode(service_config, service_names) validate_depends_on(service_config, service_names) validate_links(service_config, service_names) diff --git a/compose/config/sort_services.py b/compose/config/sort_services.py index 20ac4461b..42f548a6d 100644 --- a/compose/config/sort_services.py +++ b/compose/config/sort_services.py @@ -38,6 +38,7 @@ def get_service_dependents(service_dict, services): if (name in get_service_names(service.get('links', [])) 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('pid')) or name in service.get('depends_on', [])) ] diff --git a/compose/config/validation.py b/compose/config/validation.py index 856f811c5..0b7961e5a 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -172,6 +172,21 @@ def validate_network_mode(service_config, service_names): "is undefined.".format(s=service_config, dep=dependency)) +def validate_pid_mode(service_config, service_names): + pid_mode = service_config.config.get('pid') + if not pid_mode: + return + + dependency = get_service_name_from_network_mode(pid_mode) + if not dependency: + return + if dependency not in service_names: + raise ConfigurationError( + "Service '{s.name}' uses the PID 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 7951d2974..28af45c71 100644 --- a/compose/project.py +++ b/compose/project.py @@ -24,10 +24,13 @@ from .network import get_networks from .network import ProjectNetworks from .service import BuildAction from .service import ContainerNetworkMode +from .service import ContainerPidMode from .service import ConvergenceStrategy from .service import NetworkMode +from .service import PidMode from .service import Service from .service import ServiceNetworkMode +from .service import ServicePidMode from .utils import microseconds_from_time_nano from .volume import ProjectVolumes @@ -97,6 +100,7 @@ class Project(object): network_mode = project.get_network_mode( service_dict, list(service_networks.keys()) ) + pid_mode = project.get_pid_mode(service_dict) volumes_from = get_volumes_from(project, service_dict) if config_data.version != V1: @@ -121,6 +125,7 @@ class Project(object): network_mode=network_mode, volumes_from=volumes_from, secrets=secrets, + pid_mode=pid_mode, **service_dict) ) @@ -224,6 +229,27 @@ class Project(object): return NetworkMode(network_mode) + def get_pid_mode(self, service_dict): + pid_mode = service_dict.pop('pid', None) + if not pid_mode: + return PidMode(None) + + service_name = get_service_name_from_network_mode(pid_mode) + if service_name: + return ServicePidMode(self.get_service(service_name)) + + container_name = get_container_name_from_network_mode(pid_mode) + if container_name: + try: + return ContainerPidMode(Container.from_id(self.client, container_name)) + except APIError: + raise ConfigurationError( + "Service '{name}' uses the PID namespace of container '{dep}' which " + "does not exist.".format(name=service_dict['name'], dep=container_name) + ) + + return PidMode(pid_mode) + def start(self, service_names=None, **options): containers = [] diff --git a/compose/service.py b/compose/service.py index 7ee63771a..c4fd96c43 100644 --- a/compose/service.py +++ b/compose/service.py @@ -157,6 +157,7 @@ class Service(object): networks=None, secrets=None, scale=None, + pid_mode=None, **options ): self.name = name @@ -166,6 +167,7 @@ class Service(object): self.links = links or [] self.volumes_from = volumes_from or [] self.network_mode = network_mode or NetworkMode(None) + self.pid_mode = pid_mode or PidMode(None) self.networks = networks or {} self.secrets = secrets or [] self.scale_num = scale or 1 @@ -607,15 +609,19 @@ class Service(object): def get_dependency_names(self): net_name = self.network_mode.service_name + pid_namespace = self.pid_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 []) + 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 + configs = dict( [(name, None) for name in self.get_linked_service_names()] ) @@ -623,6 +629,7 @@ class Service(object): [(name, None) for name in self.get_volumes_from_names()] )) configs.update({net_name: None} if net_name else {}) + configs.update({pid_namespace: None} if pid_namespace else {}) configs.update(self.options.get('depends_on', {})) for svc, config in self.options.get('depends_on', {}).items(): if config['condition'] == CONDITION_STARTED: @@ -833,7 +840,7 @@ class Service(object): log_config=log_config, extra_hosts=options.get('extra_hosts'), read_only=options.get('read_only'), - pid_mode=options.get('pid'), + pid_mode=self.pid_mode.mode, security_opt=options.get('security_opt'), ipc_mode=options.get('ipc'), cgroup_parent=options.get('cgroup_parent'), @@ -1056,6 +1063,46 @@ def short_id_alias_exists(container, network): return container.short_id in aliases +class PidMode(object): + def __init__(self, mode): + self._mode = mode + + @property + def mode(self): + return self._mode + + @property + def service_name(self): + return None + + +class ServicePidMode(PidMode): + 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.warn( + "Service %s is trying to use reuse the PID namespace " + "of another service that is not running." % (self.service_name) + ) + return None + + +class ContainerPidMode(PidMode): + def __init__(self, container): + self.container = container + self._mode = 'container:{}'.format(container.id) + + class NetworkMode(object): """A `standard` network mode (ex: host, bridge)""" diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index ba0b53888..9058fa35b 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1183,6 +1183,31 @@ class CLITestCase(DockerClientTestCase): proc.wait() self.assertEqual(proc.returncode, 1) + @v2_only() + def test_up_with_pid_mode(self): + c = self.client.create_container( + 'busybox', 'top', name='composetest_pid_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/pid-mode' + + self.dispatch(['up', '-d'], None) + + service_mode_source = 'container:{}'.format( + self.project.get_service('container').containers()[0].id) + service_mode_container = self.project.get_service('service').containers()[0] + assert service_mode_container.get('HostConfig.PidMode') == service_mode_source + + container_mode_container = self.project.get_service('container').containers()[0] + assert container_mode_container.get('HostConfig.PidMode') == container_mode_source + + host_mode_container = self.project.get_service('host').containers()[0] + assert host_mode_container.get('HostConfig.PidMode') == 'host' + def test_exec_without_tty(self): self.base_dir = 'tests/fixtures/links-composefile' self.dispatch(['up', '-d', 'console']) diff --git a/tests/fixtures/pid-mode/docker-compose.yml b/tests/fixtures/pid-mode/docker-compose.yml new file mode 100644 index 000000000..fece5a9f0 --- /dev/null +++ b/tests/fixtures/pid-mode/docker-compose.yml @@ -0,0 +1,17 @@ +version: "2.2" + +services: + service: + image: busybox + command: top + pid: "service:container" + + container: + image: busybox + command: top + pid: "container:composetest_pid_mode_container" + + host: + image: busybox + command: top + pid: host diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index c406a8d5e..ccd6c8b00 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -36,6 +36,7 @@ from compose.project import OneOffFilter from compose.service import ConvergencePlan from compose.service import ConvergenceStrategy from compose.service import NetworkMode +from compose.service import PidMode from compose.service import Service from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_2_only @@ -968,12 +969,12 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(container.get('HostConfig.NetworkMode'), 'host') def test_pid_mode_none_defined(self): - service = self.create_service('web', pid=None) + service = self.create_service('web', pid_mode=None) container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.PidMode'), '') def test_pid_mode_host(self): - service = self.create_service('web', pid='host') + service = self.create_service('web', pid_mode=PidMode('host')) container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.PidMode'), 'host')