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]
|
Usage: down [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--rmi type Remove images, type may be one of: 'all' to remove
|
--rmi type Remove images, type may be one of: 'all' to remove
|
||||||
all images, or 'local' to remove only images that
|
all images, or 'local' to remove only images that
|
||||||
don't have an custom name set by the `image` field
|
don't have an custom name set by the `image` field
|
||||||
-v, --volumes Remove data volumes
|
-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'])
|
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):
|
def events(self, options):
|
||||||
"""
|
"""
|
||||||
|
@ -324,9 +326,9 @@ class TopLevelCommand(object):
|
||||||
signals.set_signal_handler_to_shutdown()
|
signals.set_signal_handler_to_shutdown()
|
||||||
try:
|
try:
|
||||||
operation = ExecOperation(
|
operation = ExecOperation(
|
||||||
self.project.client,
|
self.project.client,
|
||||||
exec_id,
|
exec_id,
|
||||||
interactive=tty,
|
interactive=tty,
|
||||||
)
|
)
|
||||||
pty = PseudoTerminal(self.project.client, operation)
|
pty = PseudoTerminal(self.project.client, operation)
|
||||||
pty.start()
|
pty.start()
|
||||||
|
@ -692,12 +694,15 @@ class TopLevelCommand(object):
|
||||||
-t, --timeout TIMEOUT Use this timeout in seconds for container shutdown
|
-t, --timeout TIMEOUT Use this timeout in seconds for container shutdown
|
||||||
when attached or when containers are already
|
when attached or when containers are already
|
||||||
running. (default: 10)
|
running. (default: 10)
|
||||||
|
--remove-orphans Remove containers for services not
|
||||||
|
defined in the Compose file
|
||||||
"""
|
"""
|
||||||
monochrome = options['--no-color']
|
monochrome = options['--no-color']
|
||||||
start_deps = not options['--no-deps']
|
start_deps = not options['--no-deps']
|
||||||
cascade_stop = options['--abort-on-container-exit']
|
cascade_stop = options['--abort-on-container-exit']
|
||||||
service_names = options['SERVICE']
|
service_names = options['SERVICE']
|
||||||
timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
|
timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
|
||||||
|
remove_orphans = options['--remove-orphans']
|
||||||
detached = options.get('-d')
|
detached = options.get('-d')
|
||||||
|
|
||||||
if detached and cascade_stop:
|
if detached and cascade_stop:
|
||||||
|
@ -710,7 +715,8 @@ class TopLevelCommand(object):
|
||||||
strategy=convergence_strategy_from_opts(options),
|
strategy=convergence_strategy_from_opts(options),
|
||||||
do_build=build_action_from_opts(options),
|
do_build=build_action_from_opts(options),
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
detached=detached)
|
detached=detached,
|
||||||
|
remove_orphans=remove_orphans)
|
||||||
|
|
||||||
if detached:
|
if detached:
|
||||||
return
|
return
|
||||||
|
|
|
@ -252,9 +252,11 @@ class Project(object):
|
||||||
def remove_stopped(self, service_names=None, **options):
|
def remove_stopped(self, service_names=None, **options):
|
||||||
parallel.parallel_remove(self.containers(service_names, stopped=True), 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.stop()
|
||||||
|
self.find_orphan_containers(remove_orphans)
|
||||||
self.remove_stopped(v=include_volumes)
|
self.remove_stopped(v=include_volumes)
|
||||||
|
|
||||||
self.networks.remove()
|
self.networks.remove()
|
||||||
|
|
||||||
if include_volumes:
|
if include_volumes:
|
||||||
|
@ -334,7 +336,8 @@ class Project(object):
|
||||||
strategy=ConvergenceStrategy.changed,
|
strategy=ConvergenceStrategy.changed,
|
||||||
do_build=BuildAction.none,
|
do_build=BuildAction.none,
|
||||||
timeout=DEFAULT_TIMEOUT,
|
timeout=DEFAULT_TIMEOUT,
|
||||||
detached=False):
|
detached=False,
|
||||||
|
remove_orphans=False):
|
||||||
|
|
||||||
self.initialize()
|
self.initialize()
|
||||||
services = self.get_services_without_duplicate(
|
services = self.get_services_without_duplicate(
|
||||||
|
@ -346,6 +349,8 @@ class Project(object):
|
||||||
for svc in services:
|
for svc in services:
|
||||||
svc.ensure_image_exists(do_build=do_build)
|
svc.ensure_image_exists(do_build=do_build)
|
||||||
|
|
||||||
|
self.find_orphan_containers(remove_orphans)
|
||||||
|
|
||||||
def do(service):
|
def do(service):
|
||||||
return service.execute_convergence_plan(
|
return service.execute_convergence_plan(
|
||||||
plans[service.name],
|
plans[service.name],
|
||||||
|
@ -402,23 +407,52 @@ class Project(object):
|
||||||
for service in self.get_services(service_names, include_deps=False):
|
for service in self.get_services(service_names, include_deps=False):
|
||||||
service.pull(ignore_pull_failures)
|
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):
|
def containers(self, service_names=None, stopped=False, one_off=False):
|
||||||
if service_names:
|
if service_names:
|
||||||
self.validate_service_names(service_names)
|
self.validate_service_names(service_names)
|
||||||
else:
|
else:
|
||||||
service_names = self.service_names
|
service_names = self.service_names
|
||||||
|
|
||||||
containers = list(filter(None, [
|
containers = self._labeled_containers(stopped, one_off)
|
||||||
Container.from_ps(self.client, container)
|
|
||||||
for container in self.client.containers(
|
|
||||||
all=stopped,
|
|
||||||
filters={'label': self.labels(one_off=one_off)})]))
|
|
||||||
|
|
||||||
def matches_service_names(container):
|
def matches_service_names(container):
|
||||||
return container.labels.get(LABEL_SERVICE) in service_names
|
return container.labels.get(LABEL_SERVICE) in service_names
|
||||||
|
|
||||||
return [c for c in containers if matches_service_names(c)]
|
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):
|
def _inject_deps(self, acc, service):
|
||||||
dep_names = service.get_dependency_names()
|
dep_names = service.get_dependency_names()
|
||||||
|
|
||||||
|
|
|
@ -161,7 +161,7 @@ _docker_compose_down() {
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--help --rmi --volumes -v" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--help --rmi --volumes -v --remove-orphans" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
@ -406,7 +406,7 @@ _docker_compose_up() {
|
||||||
|
|
||||||
case "$cur" in
|
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
|
__docker_compose_services_all
|
||||||
|
|
|
@ -18,9 +18,11 @@ created by `up`. Only containers and networks are removed by default.
|
||||||
Usage: down [options]
|
Usage: down [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--rmi type Remove images, type may be one of: 'all' to remove
|
--rmi type Remove images, type may be one of: 'all' to remove
|
||||||
all images, or 'local' to remove only images that
|
all images, or 'local' to remove only images that
|
||||||
don't have an custom name set by the `image` field
|
don't have an custom name set by the `image` field
|
||||||
-v, --volumes Remove data volumes
|
-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
|
-t, --timeout TIMEOUT Use this timeout in seconds for container shutdown
|
||||||
when attached or when containers are already
|
when attached or when containers are already
|
||||||
running. (default: 10)
|
running. (default: 10)
|
||||||
|
--remove-orphans Remove containers for services not defined in
|
||||||
|
the Compose file
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import py
|
||||||
import pytest
|
import pytest
|
||||||
from docker.errors import NotFound
|
from docker.errors import NotFound
|
||||||
|
|
||||||
|
from .. import mock
|
||||||
from ..helpers import build_config
|
from ..helpers import build_config
|
||||||
from .testcases import DockerClientTestCase
|
from .testcases import DockerClientTestCase
|
||||||
from compose.config import config
|
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 VolumeFromSpec
|
||||||
from compose.config.types import VolumeSpec
|
from compose.config.types import VolumeSpec
|
||||||
from compose.const import LABEL_PROJECT
|
from compose.const import LABEL_PROJECT
|
||||||
|
from compose.const import LABEL_SERVICE
|
||||||
from compose.container import Container
|
from compose.container import Container
|
||||||
from compose.project import Project
|
from compose.project import Project
|
||||||
from compose.service import ConvergenceStrategy
|
from compose.service import ConvergenceStrategy
|
||||||
|
@ -1055,3 +1057,40 @@ class ProjectTest(DockerClientTestCase):
|
||||||
container = service.get_container()
|
container = service.get_container()
|
||||||
assert [mount['Name'] for mount in container.get('Mounts')] == [full_vol_name]
|
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
|
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