diff --git a/compose/cli/main.py b/compose/cli/main.py index 8c1a55ffe..9957d391f 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -247,7 +247,7 @@ class TopLevelCommand(DocoptCommand): def down(self, project, options): """ Stop containers and remove containers, networks, volumes, and images - created by `up`. + created by `up`. Only containers and networks are removed by default. Usage: down [options] diff --git a/compose/parallel.py b/compose/parallel.py index 2735a397f..b8415e5e5 100644 --- a/compose/parallel.py +++ b/compose/parallel.py @@ -24,7 +24,7 @@ def parallel_execute(objects, func, index_func, msg): object we give it. """ objects = list(objects) - stream = get_output_stream(sys.stdout) + stream = get_output_stream(sys.stderr) writer = ParallelStreamWriter(stream, msg) for obj in objects: diff --git a/compose/project.py b/compose/project.py index 0774a4002..3ba9532f0 100644 --- a/compose/project.py +++ b/compose/project.py @@ -272,7 +272,7 @@ class Project(object): def down(self, remove_image_type, include_volumes): self.stop() - self.remove_stopped() + self.remove_stopped(v=include_volumes) self.remove_network() if include_volumes: @@ -441,6 +441,7 @@ class Project(object): return network = self.get_network() if network: + log.info("Removing network %s", self.default_network_name) self.client.remove_network(network['Id']) def uses_default_network(self): diff --git a/compose/service.py b/compose/service.py index 1972b1b14..1c848ca39 100644 --- a/compose/service.py +++ b/compose/service.py @@ -686,6 +686,7 @@ class Service(object): 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 diff --git a/compose/volume.py b/compose/volume.py index b78aa029f..469e406a8 100644 --- a/compose/volume.py +++ b/compose/volume.py @@ -1,9 +1,14 @@ from __future__ import absolute_import from __future__ import unicode_literals +import logging + from docker.errors import NotFound +log = logging.getLogger(__name__) + + class Volume(object): def __init__(self, client, project, name, driver=None, driver_opts=None, external_name=None): @@ -20,6 +25,10 @@ class Volume(object): ) 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) def inspect(self): diff --git a/docs/index.md b/docs/index.md index 6e8f2090c..887df99d6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -154,8 +154,7 @@ environments in just a few commands: $ docker-compose up -d $ ./run_tests - $ docker-compose stop - $ docker-compose rm -f + $ docker-compose down ### Single host deployments diff --git a/docs/reference/down.md b/docs/reference/down.md new file mode 100644 index 000000000..428e4e58a --- /dev/null +++ b/docs/reference/down.md @@ -0,0 +1,26 @@ + + +# 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 + +``` diff --git a/docs/reference/index.md b/docs/reference/index.md index 1635b60c7..5406b9c7d 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -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. * [build](build.md) +* [config](config.md) +* [create](create.md) +* [down](down.md) * [events](events.md) * [help](help.md) * [kill](kill.md) * [logs](logs.md) +* [pause](pause.md) * [port](port.md) * [ps](ps.md) * [pull](pull.md) @@ -27,6 +31,7 @@ The following pages describe the usage information for the [docker-compose](dock * [scale](scale.md) * [start](start.md) * [stop](stop.md) +* [unpause](unpause.md) * [up](up.md) ## Where to go next diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index a06f14dcc..cb04918bd 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -319,8 +319,16 @@ class CLITestCase(DockerClientTestCase): assert '--rmi flag must be' in result.stderr def test_down(self): - result = self.dispatch(['down']) - # TODO: + 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): self.dispatch(['up', '-d']) diff --git a/tests/fixtures/shutdown/Dockerfile b/tests/fixtures/shutdown/Dockerfile new file mode 100644 index 000000000..51ed0d907 --- /dev/null +++ b/tests/fixtures/shutdown/Dockerfile @@ -0,0 +1,4 @@ + +FROM busybox:latest +RUN echo something +CMD top diff --git a/tests/fixtures/shutdown/docker-compose.yml b/tests/fixtures/shutdown/docker-compose.yml new file mode 100644 index 000000000..c83c3d637 --- /dev/null +++ b/tests/fixtures/shutdown/docker-compose.yml @@ -0,0 +1,10 @@ + +version: 2 + +volumes: + data: + driver: local + +services: + web: + build: . diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 4818e47aa..314076cdf 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -616,13 +616,13 @@ class ServiceTest(DockerClientTestCase): service.create_container(number=next_number) 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) for container in service.containers(): self.assertTrue(container.is_running) self.assertTrue(container.number in valid_numbers) - captured_output = mock_stdout.getvalue() + captured_output = mock_stderr.getvalue() self.assertNotIn('Creating', captured_output) self.assertIn('Starting', captured_output) @@ -639,14 +639,14 @@ class ServiceTest(DockerClientTestCase): for container in service.containers(): 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) self.assertEqual(len(service.containers()), 2) for container in service.containers(): self.assertTrue(container.is_running) - captured_output = mock_stdout.getvalue() + captured_output = mock_stderr.getvalue() self.assertIn('Creating', captured_output) self.assertIn('Starting', captured_output) @@ -665,12 +665,12 @@ class ServiceTest(DockerClientTestCase): response={}, 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) self.assertEqual(len(service.containers()), 1) 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): """Test that when scaling if the API returns an error, that is not of type diff --git a/tests/unit/volume_test.py b/tests/unit/volume_test.py new file mode 100644 index 000000000..d7ad07928 --- /dev/null +++ b/tests/unit/volume_test.py @@ -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