mirror of https://github.com/docker/compose.git
Adding docker-compose down
Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
parent
18df1e170d
commit
c8ed156806
|
@ -25,6 +25,7 @@ from ..progress_stream import StreamOutputError
|
|||
from ..project import NoSuchService
|
||||
from ..service import BuildError
|
||||
from ..service import ConvergenceStrategy
|
||||
from ..service import ImageType
|
||||
from ..service import NeedsBuildError
|
||||
from .command import friendly_error_message
|
||||
from .command import get_config_path_from_options
|
||||
|
@ -129,6 +130,7 @@ class TopLevelCommand(DocoptCommand):
|
|||
build Build or rebuild services
|
||||
config Validate and view the compose file
|
||||
create Create services
|
||||
down Stop and remove containers, networks, images, and volumes
|
||||
events Receive real time events from containers
|
||||
help Get help on a command
|
||||
kill Kill containers
|
||||
|
@ -242,6 +244,22 @@ class TopLevelCommand(DocoptCommand):
|
|||
do_build=not options['--no-build']
|
||||
)
|
||||
|
||||
def down(self, project, options):
|
||||
"""
|
||||
Stop containers and remove containers, networks, volumes, and images
|
||||
created by `up`.
|
||||
|
||||
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):
|
||||
"""
|
||||
Receive real time events from containers.
|
||||
|
@ -660,6 +678,15 @@ def convergence_strategy_from_opts(options):
|
|||
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):
|
||||
if not options['--no-deps']:
|
||||
deps = service.get_linked_service_names()
|
||||
|
|
|
@ -270,6 +270,24 @@ class Project(object):
|
|||
)
|
||||
)
|
||||
|
||||
def down(self, remove_image_type, include_volumes):
|
||||
self.stop()
|
||||
self.remove_stopped()
|
||||
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):
|
||||
containers = self.containers(service_names, stopped=True)
|
||||
parallel.parallel_restart(containers, options)
|
||||
|
@ -419,6 +437,8 @@ class Project(object):
|
|||
self.client.create_network(self.default_network_name, driver=self.network_driver)
|
||||
|
||||
def remove_network(self):
|
||||
if not self.use_networking:
|
||||
return
|
||||
network = self.get_network()
|
||||
if network:
|
||||
self.client.remove_network(network['Id'])
|
||||
|
|
|
@ -98,6 +98,14 @@ class ConvergenceStrategy(enum.Enum):
|
|||
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):
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -672,6 +680,19 @@ class Service(object):
|
|||
def custom_container_name(self):
|
||||
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
|
||||
|
||||
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 has_host_port(binding):
|
||||
_, external_bindings = split_port(binding)
|
||||
|
|
|
@ -314,6 +314,14 @@ class CLITestCase(DockerClientTestCase):
|
|||
['create', '--force-recreate', '--no-recreate'],
|
||||
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):
|
||||
result = self.dispatch(['down'])
|
||||
# TODO:
|
||||
|
||||
def test_up_detached(self):
|
||||
self.dispatch(['up', '-d'])
|
||||
service = self.project.get_service('simple')
|
||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import absolute_import
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import docker
|
||||
from docker.errors import APIError
|
||||
|
||||
from .. import mock
|
||||
from .. import unittest
|
||||
|
@ -16,6 +17,7 @@ from compose.service import build_ulimits
|
|||
from compose.service import build_volume_binding
|
||||
from compose.service import ContainerNet
|
||||
from compose.service import get_container_data_volumes
|
||||
from compose.service import ImageType
|
||||
from compose.service import merge_volume_bindings
|
||||
from compose.service import NeedsBuildError
|
||||
from compose.service import Net
|
||||
|
@ -422,6 +424,38 @@ class ServiceTest(unittest.TestCase):
|
|||
}
|
||||
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):
|
||||
service = Service(
|
||||
'foo',
|
||||
|
|
Loading…
Reference in New Issue