1
0
Эх сурвалжийг харах

Add flag to up/down to remove orphaned containers
Add --remove-orphans to CLI reference docs
Add --remove-orphans to bash completion file
Test orphan warning and remove_orphan option in up

Signed-off-by: Joffrey F <[email protected]>

Joffrey F 9 жил өмнө
parent
commit
20c29f7e47

+ 15 - 9
compose/cli/main.py

@@ -252,13 +252,15 @@ class TopLevelCommand(object):
         Usage: down [options]
 
         Options:
-            --rmi type      Remove images, type may be one of: 'all' to remove
-                            all images, or 'local' to remove only images that
-                            don't have an custom name set by the `image` field
-            -v, --volumes   Remove data volumes
+            --rmi type          Remove images, type may be one of: 'all' to remove
+                                all images, or 'local' to remove only images that
+                                don't have an custom name set by the `image` field
+            -v, --volumes       Remove data volumes
+            --remove-orphans    Remove containers for services not defined in
+                                the Compose file
         """
         image_type = image_type_from_opt('--rmi', options['--rmi'])
-        self.project.down(image_type, options['--volumes'])
+        self.project.down(image_type, options['--volumes'], options['--remove-orphans'])
 
     def events(self, options):
         """
@@ -324,9 +326,9 @@ class TopLevelCommand(object):
         signals.set_signal_handler_to_shutdown()
         try:
             operation = ExecOperation(
-                    self.project.client,
-                    exec_id,
-                    interactive=tty,
+                self.project.client,
+                exec_id,
+                interactive=tty,
             )
             pty = PseudoTerminal(self.project.client, operation)
             pty.start()
@@ -692,12 +694,15 @@ class TopLevelCommand(object):
             -t, --timeout TIMEOUT      Use this timeout in seconds for container shutdown
                                        when attached or when containers are already
                                        running. (default: 10)
+            --remove-orphans           Remove containers for services not
+                                       defined in the Compose file
         """
         monochrome = options['--no-color']
         start_deps = not options['--no-deps']
         cascade_stop = options['--abort-on-container-exit']
         service_names = options['SERVICE']
         timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
+        remove_orphans = options['--remove-orphans']
         detached = options.get('-d')
 
         if detached and cascade_stop:
@@ -710,7 +715,8 @@ class TopLevelCommand(object):
                 strategy=convergence_strategy_from_opts(options),
                 do_build=build_action_from_opts(options),
                 timeout=timeout,
-                detached=detached)
+                detached=detached,
+                remove_orphans=remove_orphans)
 
             if detached:
                 return

+ 41 - 7
compose/project.py

@@ -252,9 +252,11 @@ class Project(object):
     def remove_stopped(self, service_names=None, **options):
         parallel.parallel_remove(self.containers(service_names, stopped=True), options)
 
-    def down(self, remove_image_type, include_volumes):
+    def down(self, remove_image_type, include_volumes, remove_orphans=False):
         self.stop()
+        self.find_orphan_containers(remove_orphans)
         self.remove_stopped(v=include_volumes)
+
         self.networks.remove()
 
         if include_volumes:
@@ -334,7 +336,8 @@ class Project(object):
            strategy=ConvergenceStrategy.changed,
            do_build=BuildAction.none,
            timeout=DEFAULT_TIMEOUT,
-           detached=False):
+           detached=False,
+           remove_orphans=False):
 
         self.initialize()
         services = self.get_services_without_duplicate(
@@ -346,6 +349,8 @@ class Project(object):
         for svc in services:
             svc.ensure_image_exists(do_build=do_build)
 
+        self.find_orphan_containers(remove_orphans)
+
         def do(service):
             return service.execute_convergence_plan(
                 plans[service.name],
@@ -402,23 +407,52 @@ class Project(object):
         for service in self.get_services(service_names, include_deps=False):
             service.pull(ignore_pull_failures)
 
+    def _labeled_containers(self, stopped=False, one_off=False):
+        return list(filter(None, [
+            Container.from_ps(self.client, container)
+            for container in self.client.containers(
+                all=stopped,
+                filters={'label': self.labels(one_off=one_off)})])
+        )
+
     def containers(self, service_names=None, stopped=False, one_off=False):
         if service_names:
             self.validate_service_names(service_names)
         else:
             service_names = self.service_names
 
-        containers = list(filter(None, [
-            Container.from_ps(self.client, container)
-            for container in self.client.containers(
-                all=stopped,
-                filters={'label': self.labels(one_off=one_off)})]))
+        containers = self._labeled_containers(stopped, one_off)
 
         def matches_service_names(container):
             return container.labels.get(LABEL_SERVICE) in service_names
 
         return [c for c in containers if matches_service_names(c)]
 
+    def find_orphan_containers(self, remove_orphans):
+        def _find():
+            containers = self._labeled_containers()
+            for ctnr in containers:
+                service_name = ctnr.labels.get(LABEL_SERVICE)
+                if service_name not in self.service_names:
+                    yield ctnr
+        orphans = list(_find())
+        if not orphans:
+            return
+        if remove_orphans:
+            for ctnr in orphans:
+                log.info('Removing orphan container "{0}"'.format(ctnr.name))
+                ctnr.kill()
+                ctnr.remove(force=True)
+        else:
+            log.warning(
+                'Found orphan containers ({0}) for this project. If '
+                'you removed or renamed this service in your compose '
+                'file, you can run this command with the '
+                '--remove-orphans flag to clean it up.'.format(
+                    ', '.join(["{}".format(ctnr.name) for ctnr in orphans])
+                )
+            )
+
     def _inject_deps(self, acc, service):
         dep_names = service.get_dependency_names()
 

+ 2 - 2
contrib/completion/bash/docker-compose

@@ -161,7 +161,7 @@ _docker_compose_down() {
 
 	case "$cur" in
 		-*)
-			COMPREPLY=( $( compgen -W "--help --rmi --volumes -v" -- "$cur" ) )
+			COMPREPLY=( $( compgen -W "--help --rmi --volumes -v --remove-orphans" -- "$cur" ) )
 			;;
 	esac
 }
@@ -406,7 +406,7 @@ _docker_compose_up() {
 
 	case "$cur" in
 		-*)
-			COMPREPLY=( $( compgen -W "--abort-on-container-exit -d --force-recreate --help --no-build --no-color --no-deps --no-recreate --timeout -t" -- "$cur" ) )
+			COMPREPLY=( $( compgen -W "--abort-on-container-exit -d --force-recreate --help --no-build --no-color --no-deps --no-recreate --timeout -t --remove-orphans" -- "$cur" ) )
 			;;
 		*)
 			__docker_compose_services_all

+ 6 - 4
docs/reference/down.md

@@ -18,9 +18,11 @@ created by `up`. Only containers and networks are removed by default.
 Usage: down [options]
 
 Options:
-    --rmi type      Remove images, type may be one of: 'all' to remove
-                    all images, or 'local' to remove only images that
-                    don't have an custom name set by the `image` field
-    -v, --volumes   Remove data volumes
+    --rmi type          Remove images, type may be one of: 'all' to remove
+                        all images, or 'local' to remove only images that
+                        don't have an custom name set by the `image` field
+    -v, --volumes       Remove data volumes
 
+    --remove-orphans    Remove containers for services not defined in the
+                        Compose file
 ```

+ 2 - 0
docs/reference/up.md

@@ -32,6 +32,8 @@ Options:
     -t, --timeout TIMEOUT      Use this timeout in seconds for container shutdown
                                when attached or when containers are already
                                running. (default: 10)
+    --remove-orphans           Remove containers for services not defined in
+                               the Compose file
 
 ```
 

+ 39 - 0
tests/integration/project_test.py

@@ -7,6 +7,7 @@ import py
 import pytest
 from docker.errors import NotFound
 
+from .. import mock
 from ..helpers import build_config
 from .testcases import DockerClientTestCase
 from compose.config import config
@@ -15,6 +16,7 @@ from compose.config.config import V2_0
 from compose.config.types import VolumeFromSpec
 from compose.config.types import VolumeSpec
 from compose.const import LABEL_PROJECT
+from compose.const import LABEL_SERVICE
 from compose.container import Container
 from compose.project import Project
 from compose.service import ConvergenceStrategy
@@ -1055,3 +1057,40 @@ class ProjectTest(DockerClientTestCase):
         container = service.get_container()
         assert [mount['Name'] for mount in container.get('Mounts')] == [full_vol_name]
         assert next((v for v in engine_volumes if v['Name'] == vol_name), None) is None
+
+    def test_project_up_orphans(self):
+        config_dict = {
+            'service1': {
+                'image': 'busybox:latest',
+                'command': 'top',
+            }
+        }
+
+        config_data = build_config(config_dict)
+        project = Project.from_config(
+            name='composetest', config_data=config_data, client=self.client
+        )
+        project.up()
+        config_dict['service2'] = config_dict['service1']
+        del config_dict['service1']
+
+        config_data = build_config(config_dict)
+        project = Project.from_config(
+            name='composetest', config_data=config_data, client=self.client
+        )
+        with mock.patch('compose.project.log') as mock_log:
+            project.up()
+
+        mock_log.warning.assert_called_once_with(mock.ANY)
+
+        assert len([
+            ctnr for ctnr in project._labeled_containers()
+            if ctnr.labels.get(LABEL_SERVICE) == 'service1'
+        ]) == 1
+
+        project.up(remove_orphans=True)
+
+        assert len([
+            ctnr for ctnr in project._labeled_containers()
+            if ctnr.labels.get(LABEL_SERVICE) == 'service1'
+        ]) == 0