Add an acceptance test and docs for the down subcommand

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2016-01-12 17:33:17 -05:00
parent c8ed156806
commit c64af0a459
13 changed files with 102 additions and 13 deletions

View File

@ -247,7 +247,7 @@ class TopLevelCommand(DocoptCommand):
def down(self, project, options): def down(self, project, options):
""" """
Stop containers and remove containers, networks, volumes, and images 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] Usage: down [options]

View File

@ -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:

View File

@ -272,7 +272,7 @@ class Project(object):
def down(self, remove_image_type, include_volumes): def down(self, remove_image_type, include_volumes):
self.stop() self.stop()
self.remove_stopped() self.remove_stopped(v=include_volumes)
self.remove_network() self.remove_network()
if include_volumes: if include_volumes:
@ -441,6 +441,7 @@ class Project(object):
return 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):

View File

@ -686,6 +686,7 @@ class Service(object):
if image_type == ImageType.local and self.options.get('image'): if image_type == ImageType.local and self.options.get('image'):
return False return False
log.info("Removing image %s", self.image_name)
try: try:
self.client.remove_image(self.image_name) self.client.remove_image(self.image_name)
return True return True

View File

@ -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):

View File

@ -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
View 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
```

View File

@ -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

View File

@ -319,8 +319,16 @@ class CLITestCase(DockerClientTestCase):
assert '--rmi flag must be' in result.stderr assert '--rmi flag must be' in result.stderr
def test_down(self): def test_down(self):
result = self.dispatch(['down']) self.base_dir = 'tests/fixtures/shutdown'
# TODO: 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'])

4
tests/fixtures/shutdown/Dockerfile vendored Normal file
View File

@ -0,0 +1,4 @@
FROM busybox:latest
RUN echo something
CMD top

View File

@ -0,0 +1,10 @@
version: 2
volumes:
data:
driver: local
services:
web:
build: .

View File

@ -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

26
tests/unit/volume_test.py Normal file
View 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