mirror of
https://github.com/docker/compose.git
synced 2025-07-22 21:24:38 +02:00
Merge pull request #2646 from dnephin/docker_compose_down
docker-compose down
This commit is contained in:
commit
172b955d79
@ -25,6 +25,7 @@ from ..progress_stream import StreamOutputError
|
|||||||
from ..project import NoSuchService
|
from ..project import NoSuchService
|
||||||
from ..service import BuildError
|
from ..service import BuildError
|
||||||
from ..service import ConvergenceStrategy
|
from ..service import ConvergenceStrategy
|
||||||
|
from ..service import ImageType
|
||||||
from ..service import NeedsBuildError
|
from ..service import NeedsBuildError
|
||||||
from .command import friendly_error_message
|
from .command import friendly_error_message
|
||||||
from .command import get_config_path_from_options
|
from .command import get_config_path_from_options
|
||||||
@ -129,6 +130,7 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
build Build or rebuild services
|
build Build or rebuild services
|
||||||
config Validate and view the compose file
|
config Validate and view the compose file
|
||||||
create Create services
|
create Create services
|
||||||
|
down Stop and remove containers, networks, images, and volumes
|
||||||
events Receive real time events from containers
|
events Receive real time events from containers
|
||||||
help Get help on a command
|
help Get help on a command
|
||||||
kill Kill containers
|
kill Kill containers
|
||||||
@ -242,6 +244,22 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
do_build=not options['--no-build']
|
do_build=not options['--no-build']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def down(self, project, options):
|
||||||
|
"""
|
||||||
|
Stop containers and remove containers, networks, volumes, and images
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
image_type = image_type_from_opt('--rmi', options['--rmi'])
|
||||||
|
project.down(image_type, options['--volumes'])
|
||||||
|
|
||||||
def events(self, project, options):
|
def events(self, project, options):
|
||||||
"""
|
"""
|
||||||
Receive real time events from containers.
|
Receive real time events from containers.
|
||||||
@ -660,6 +678,15 @@ def convergence_strategy_from_opts(options):
|
|||||||
return ConvergenceStrategy.changed
|
return ConvergenceStrategy.changed
|
||||||
|
|
||||||
|
|
||||||
|
def image_type_from_opt(flag, value):
|
||||||
|
if not value:
|
||||||
|
return ImageType.none
|
||||||
|
try:
|
||||||
|
return ImageType[value]
|
||||||
|
except KeyError:
|
||||||
|
raise UserError("%s flag must be one of: all, local" % flag)
|
||||||
|
|
||||||
|
|
||||||
def run_one_off_container(container_options, project, service, options):
|
def run_one_off_container(container_options, project, service, options):
|
||||||
if not options['--no-deps']:
|
if not options['--no-deps']:
|
||||||
deps = service.get_linked_service_names()
|
deps = service.get_linked_service_names()
|
||||||
|
@ -24,7 +24,7 @@ def parallel_execute(objects, func, index_func, msg):
|
|||||||
object we give it.
|
object we give it.
|
||||||
"""
|
"""
|
||||||
objects = list(objects)
|
objects = list(objects)
|
||||||
stream = get_output_stream(sys.stdout)
|
stream = get_output_stream(sys.stderr)
|
||||||
writer = ParallelStreamWriter(stream, msg)
|
writer = ParallelStreamWriter(stream, msg)
|
||||||
|
|
||||||
for obj in objects:
|
for obj in objects:
|
||||||
|
@ -270,6 +270,24 @@ class Project(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def down(self, remove_image_type, include_volumes):
|
||||||
|
self.stop()
|
||||||
|
self.remove_stopped(v=include_volumes)
|
||||||
|
self.remove_network()
|
||||||
|
|
||||||
|
if include_volumes:
|
||||||
|
self.remove_volumes()
|
||||||
|
|
||||||
|
self.remove_images(remove_image_type)
|
||||||
|
|
||||||
|
def remove_images(self, remove_image_type):
|
||||||
|
for service in self.get_services():
|
||||||
|
service.remove_image(remove_image_type)
|
||||||
|
|
||||||
|
def remove_volumes(self):
|
||||||
|
for volume in self.volumes:
|
||||||
|
volume.remove()
|
||||||
|
|
||||||
def restart(self, service_names=None, **options):
|
def restart(self, service_names=None, **options):
|
||||||
containers = self.containers(service_names, stopped=True)
|
containers = self.containers(service_names, stopped=True)
|
||||||
parallel.parallel_restart(containers, options)
|
parallel.parallel_restart(containers, options)
|
||||||
@ -419,8 +437,11 @@ class Project(object):
|
|||||||
self.client.create_network(self.default_network_name, driver=self.network_driver)
|
self.client.create_network(self.default_network_name, driver=self.network_driver)
|
||||||
|
|
||||||
def remove_network(self):
|
def remove_network(self):
|
||||||
|
if not self.use_networking:
|
||||||
|
return
|
||||||
network = self.get_network()
|
network = self.get_network()
|
||||||
if network:
|
if network:
|
||||||
|
log.info("Removing network %s", self.default_network_name)
|
||||||
self.client.remove_network(network['Id'])
|
self.client.remove_network(network['Id'])
|
||||||
|
|
||||||
def uses_default_network(self):
|
def uses_default_network(self):
|
||||||
|
@ -98,6 +98,14 @@ class ConvergenceStrategy(enum.Enum):
|
|||||||
return self is not type(self).never
|
return self is not type(self).never
|
||||||
|
|
||||||
|
|
||||||
|
@enum.unique
|
||||||
|
class ImageType(enum.Enum):
|
||||||
|
"""Enumeration for the types of images known to compose."""
|
||||||
|
none = 0
|
||||||
|
local = 1
|
||||||
|
all = 2
|
||||||
|
|
||||||
|
|
||||||
class Service(object):
|
class Service(object):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -672,6 +680,20 @@ class Service(object):
|
|||||||
def custom_container_name(self):
|
def custom_container_name(self):
|
||||||
return self.options.get('container_name')
|
return self.options.get('container_name')
|
||||||
|
|
||||||
|
def remove_image(self, image_type):
|
||||||
|
if not image_type or image_type == ImageType.none:
|
||||||
|
return False
|
||||||
|
if image_type == ImageType.local and self.options.get('image'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
log.info("Removing image %s", self.image_name)
|
||||||
|
try:
|
||||||
|
self.client.remove_image(self.image_name)
|
||||||
|
return True
|
||||||
|
except APIError as e:
|
||||||
|
log.error("Failed to remove image for service %s: %s", self.name, e)
|
||||||
|
return False
|
||||||
|
|
||||||
def specifies_host_port(self):
|
def specifies_host_port(self):
|
||||||
def has_host_port(binding):
|
def has_host_port(binding):
|
||||||
_, external_bindings = split_port(binding)
|
_, external_bindings = split_port(binding)
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from docker.errors import NotFound
|
from docker.errors import NotFound
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Volume(object):
|
class Volume(object):
|
||||||
def __init__(self, client, project, name, driver=None, driver_opts=None,
|
def __init__(self, client, project, name, driver=None, driver_opts=None,
|
||||||
external_name=None):
|
external_name=None):
|
||||||
@ -20,6 +25,10 @@ class Volume(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def remove(self):
|
def remove(self):
|
||||||
|
if self.external:
|
||||||
|
log.info("Volume %s is external, skipping", self.full_name)
|
||||||
|
return
|
||||||
|
log.info("Removing volume %s", self.full_name)
|
||||||
return self.client.remove_volume(self.full_name)
|
return self.client.remove_volume(self.full_name)
|
||||||
|
|
||||||
def inspect(self):
|
def inspect(self):
|
||||||
|
@ -154,8 +154,7 @@ environments in just a few commands:
|
|||||||
|
|
||||||
$ docker-compose up -d
|
$ docker-compose up -d
|
||||||
$ ./run_tests
|
$ ./run_tests
|
||||||
$ docker-compose stop
|
$ docker-compose down
|
||||||
$ docker-compose rm -f
|
|
||||||
|
|
||||||
### Single host deployments
|
### Single host deployments
|
||||||
|
|
||||||
|
26
docs/reference/down.md
Normal file
26
docs/reference/down.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!--[metadata]>
|
||||||
|
+++
|
||||||
|
title = "down"
|
||||||
|
description = "down"
|
||||||
|
keywords = ["fig, composition, compose, docker, orchestration, cli, down"]
|
||||||
|
[menu.main]
|
||||||
|
identifier="build.compose"
|
||||||
|
parent = "smn_compose_cli"
|
||||||
|
+++
|
||||||
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
# down
|
||||||
|
|
||||||
|
```
|
||||||
|
Stop containers and remove containers, networks, volumes, and images
|
||||||
|
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
|
||||||
|
|
||||||
|
```
|
@ -14,10 +14,14 @@ parent = "smn_compose_ref"
|
|||||||
The following pages describe the usage information for the [docker-compose](docker-compose.md) subcommands. You can also see this information by running `docker-compose [SUBCOMMAND] --help` from the command line.
|
The following pages describe the usage information for the [docker-compose](docker-compose.md) subcommands. You can also see this information by running `docker-compose [SUBCOMMAND] --help` from the command line.
|
||||||
|
|
||||||
* [build](build.md)
|
* [build](build.md)
|
||||||
|
* [config](config.md)
|
||||||
|
* [create](create.md)
|
||||||
|
* [down](down.md)
|
||||||
* [events](events.md)
|
* [events](events.md)
|
||||||
* [help](help.md)
|
* [help](help.md)
|
||||||
* [kill](kill.md)
|
* [kill](kill.md)
|
||||||
* [logs](logs.md)
|
* [logs](logs.md)
|
||||||
|
* [pause](pause.md)
|
||||||
* [port](port.md)
|
* [port](port.md)
|
||||||
* [ps](ps.md)
|
* [ps](ps.md)
|
||||||
* [pull](pull.md)
|
* [pull](pull.md)
|
||||||
@ -27,6 +31,7 @@ The following pages describe the usage information for the [docker-compose](dock
|
|||||||
* [scale](scale.md)
|
* [scale](scale.md)
|
||||||
* [start](start.md)
|
* [start](start.md)
|
||||||
* [stop](stop.md)
|
* [stop](stop.md)
|
||||||
|
* [unpause](unpause.md)
|
||||||
* [up](up.md)
|
* [up](up.md)
|
||||||
|
|
||||||
## Where to go next
|
## Where to go next
|
||||||
|
@ -314,6 +314,22 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
['create', '--force-recreate', '--no-recreate'],
|
['create', '--force-recreate', '--no-recreate'],
|
||||||
returncode=1)
|
returncode=1)
|
||||||
|
|
||||||
|
def test_down_invalid_rmi_flag(self):
|
||||||
|
result = self.dispatch(['down', '--rmi', 'bogus'], returncode=1)
|
||||||
|
assert '--rmi flag must be' in result.stderr
|
||||||
|
|
||||||
|
def test_down(self):
|
||||||
|
self.base_dir = 'tests/fixtures/shutdown'
|
||||||
|
self.dispatch(['up', '-d'])
|
||||||
|
wait_on_condition(ContainerCountCondition(self.project, 1))
|
||||||
|
|
||||||
|
result = self.dispatch(['down', '--rmi=local', '--volumes'])
|
||||||
|
assert 'Stopping shutdown_web_1' in result.stderr
|
||||||
|
assert 'Removing shutdown_web_1' in result.stderr
|
||||||
|
assert 'Removing volume shutdown_data' in result.stderr
|
||||||
|
assert 'Removing image shutdown_web' in result.stderr
|
||||||
|
assert 'Removing network shutdown_default' in result.stderr
|
||||||
|
|
||||||
def test_up_detached(self):
|
def test_up_detached(self):
|
||||||
self.dispatch(['up', '-d'])
|
self.dispatch(['up', '-d'])
|
||||||
service = self.project.get_service('simple')
|
service = self.project.get_service('simple')
|
||||||
|
4
tests/fixtures/shutdown/Dockerfile
vendored
Normal file
4
tests/fixtures/shutdown/Dockerfile
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
FROM busybox:latest
|
||||||
|
RUN echo something
|
||||||
|
CMD top
|
10
tests/fixtures/shutdown/docker-compose.yml
vendored
Normal file
10
tests/fixtures/shutdown/docker-compose.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
@ -616,13 +616,13 @@ class ServiceTest(DockerClientTestCase):
|
|||||||
service.create_container(number=next_number)
|
service.create_container(number=next_number)
|
||||||
service.create_container(number=next_number + 1)
|
service.create_container(number=next_number + 1)
|
||||||
|
|
||||||
with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout:
|
with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr:
|
||||||
service.scale(2)
|
service.scale(2)
|
||||||
for container in service.containers():
|
for container in service.containers():
|
||||||
self.assertTrue(container.is_running)
|
self.assertTrue(container.is_running)
|
||||||
self.assertTrue(container.number in valid_numbers)
|
self.assertTrue(container.number in valid_numbers)
|
||||||
|
|
||||||
captured_output = mock_stdout.getvalue()
|
captured_output = mock_stderr.getvalue()
|
||||||
self.assertNotIn('Creating', captured_output)
|
self.assertNotIn('Creating', captured_output)
|
||||||
self.assertIn('Starting', captured_output)
|
self.assertIn('Starting', captured_output)
|
||||||
|
|
||||||
@ -639,14 +639,14 @@ class ServiceTest(DockerClientTestCase):
|
|||||||
for container in service.containers():
|
for container in service.containers():
|
||||||
self.assertFalse(container.is_running)
|
self.assertFalse(container.is_running)
|
||||||
|
|
||||||
with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout:
|
with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr:
|
||||||
service.scale(2)
|
service.scale(2)
|
||||||
|
|
||||||
self.assertEqual(len(service.containers()), 2)
|
self.assertEqual(len(service.containers()), 2)
|
||||||
for container in service.containers():
|
for container in service.containers():
|
||||||
self.assertTrue(container.is_running)
|
self.assertTrue(container.is_running)
|
||||||
|
|
||||||
captured_output = mock_stdout.getvalue()
|
captured_output = mock_stderr.getvalue()
|
||||||
self.assertIn('Creating', captured_output)
|
self.assertIn('Creating', captured_output)
|
||||||
self.assertIn('Starting', captured_output)
|
self.assertIn('Starting', captured_output)
|
||||||
|
|
||||||
@ -665,12 +665,12 @@ class ServiceTest(DockerClientTestCase):
|
|||||||
response={},
|
response={},
|
||||||
explanation="Boom")):
|
explanation="Boom")):
|
||||||
|
|
||||||
with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout:
|
with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr:
|
||||||
service.scale(3)
|
service.scale(3)
|
||||||
|
|
||||||
self.assertEqual(len(service.containers()), 1)
|
self.assertEqual(len(service.containers()), 1)
|
||||||
self.assertTrue(service.containers()[0].is_running)
|
self.assertTrue(service.containers()[0].is_running)
|
||||||
self.assertIn("ERROR: for 2 Boom", mock_stdout.getvalue())
|
self.assertIn("ERROR: for 2 Boom", mock_stderr.getvalue())
|
||||||
|
|
||||||
def test_scale_with_unexpected_exception(self):
|
def test_scale_with_unexpected_exception(self):
|
||||||
"""Test that when scaling if the API returns an error, that is not of type
|
"""Test that when scaling if the API returns an error, that is not of type
|
||||||
|
@ -2,6 +2,7 @@ from __future__ import absolute_import
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
|
from docker.errors import APIError
|
||||||
|
|
||||||
from .. import mock
|
from .. import mock
|
||||||
from .. import unittest
|
from .. import unittest
|
||||||
@ -16,6 +17,7 @@ from compose.service import build_ulimits
|
|||||||
from compose.service import build_volume_binding
|
from compose.service import build_volume_binding
|
||||||
from compose.service import ContainerNet
|
from compose.service import ContainerNet
|
||||||
from compose.service import get_container_data_volumes
|
from compose.service import get_container_data_volumes
|
||||||
|
from compose.service import ImageType
|
||||||
from compose.service import merge_volume_bindings
|
from compose.service import merge_volume_bindings
|
||||||
from compose.service import NeedsBuildError
|
from compose.service import NeedsBuildError
|
||||||
from compose.service import Net
|
from compose.service import Net
|
||||||
@ -422,6 +424,38 @@ class ServiceTest(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
self.assertEqual(config_dict, expected)
|
self.assertEqual(config_dict, expected)
|
||||||
|
|
||||||
|
def test_remove_image_none(self):
|
||||||
|
web = Service('web', image='example', client=self.mock_client)
|
||||||
|
assert not web.remove_image(ImageType.none)
|
||||||
|
assert not self.mock_client.remove_image.called
|
||||||
|
|
||||||
|
def test_remove_image_local_with_image_name_doesnt_remove(self):
|
||||||
|
web = Service('web', image='example', client=self.mock_client)
|
||||||
|
assert not web.remove_image(ImageType.local)
|
||||||
|
assert not self.mock_client.remove_image.called
|
||||||
|
|
||||||
|
def test_remove_image_local_without_image_name_does_remove(self):
|
||||||
|
web = Service('web', build='.', client=self.mock_client)
|
||||||
|
assert web.remove_image(ImageType.local)
|
||||||
|
self.mock_client.remove_image.assert_called_once_with(web.image_name)
|
||||||
|
|
||||||
|
def test_remove_image_all_does_remove(self):
|
||||||
|
web = Service('web', image='example', client=self.mock_client)
|
||||||
|
assert web.remove_image(ImageType.all)
|
||||||
|
self.mock_client.remove_image.assert_called_once_with(web.image_name)
|
||||||
|
|
||||||
|
def test_remove_image_with_error(self):
|
||||||
|
self.mock_client.remove_image.side_effect = error = APIError(
|
||||||
|
message="testing",
|
||||||
|
response={},
|
||||||
|
explanation="Boom")
|
||||||
|
|
||||||
|
web = Service('web', image='example', client=self.mock_client)
|
||||||
|
with mock.patch('compose.service.log', autospec=True) as mock_log:
|
||||||
|
assert not web.remove_image(ImageType.all)
|
||||||
|
mock_log.error.assert_called_once_with(
|
||||||
|
"Failed to remove image for service %s: %s", web.name, error)
|
||||||
|
|
||||||
def test_specifies_host_port_with_no_ports(self):
|
def test_specifies_host_port_with_no_ports(self):
|
||||||
service = Service(
|
service = Service(
|
||||||
'foo',
|
'foo',
|
||||||
|
26
tests/unit/volume_test.py
Normal file
26
tests/unit/volume_test.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import docker
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from compose import volume
|
||||||
|
from tests import mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_client():
|
||||||
|
return mock.create_autospec(docker.Client)
|
||||||
|
|
||||||
|
|
||||||
|
class TestVolume(object):
|
||||||
|
|
||||||
|
def test_remove_local_volume(self, mock_client):
|
||||||
|
vol = volume.Volume(mock_client, 'foo', 'project')
|
||||||
|
vol.remove()
|
||||||
|
mock_client.remove_volume.assert_called_once_with('foo_project')
|
||||||
|
|
||||||
|
def test_remove_external_volume(self, mock_client):
|
||||||
|
vol = volume.Volume(mock_client, 'foo', 'project', external_name='data')
|
||||||
|
vol.remove()
|
||||||
|
assert not mock_client.remove_volume.called
|
Loading…
x
Reference in New Issue
Block a user