mirror of https://github.com/docker/compose.git
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 <joffrey@docker.com>
This commit is contained in:
parent
34de1f0a4c
commit
20c29f7e47
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue