diff --git a/compose/cli/main.py b/compose/cli/main.py index 473c6d605..4be8536f4 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -9,7 +9,6 @@ import sys from inspect import getdoc from operator import attrgetter -import yaml from docker.errors import APIError from requests.exceptions import ReadTimeout @@ -18,6 +17,7 @@ from .. import __version__ from ..config import config from ..config import ConfigurationError from ..config import parse_environment +from ..config.serialize import serialize_config from ..const import DEFAULT_TIMEOUT from ..const import HTTP_TIMEOUT from ..const import IS_WINDOWS_PLATFORM @@ -215,13 +215,7 @@ class TopLevelCommand(DocoptCommand): print('\n'.join(service['name'] for service in compose_config.services)) return - compose_config = dict( - (service.pop('name'), service) for service in compose_config.services) - print(yaml.dump( - compose_config, - default_flow_style=False, - indent=2, - width=80)) + print(serialize_config(compose_config)) def create(self, project, options): """ diff --git a/compose/config/serialize.py b/compose/config/serialize.py new file mode 100644 index 000000000..06e0a027b --- /dev/null +++ b/compose/config/serialize.py @@ -0,0 +1,30 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import six +import yaml + +from compose.config import types + + +def serialize_config_type(dumper, data): + representer = dumper.represent_str if six.PY3 else dumper.represent_unicode + return representer(data.repr()) + + +yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type) +yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type) + + +def serialize_config(config): + output = { + 'version': config.version, + 'services': {service.pop('name'): service for service in config.services}, + 'networks': config.networks, + 'volumes': config.volumes, + } + return yaml.safe_dump( + output, + default_flow_style=False, + indent=2, + width=80) diff --git a/compose/config/types.py b/compose/config/types.py index c0adca6c7..b872cba91 100644 --- a/compose/config/types.py +++ b/compose/config/types.py @@ -67,6 +67,9 @@ class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')): return cls(source, mode, type) + def repr(self): + return '{v.type}:{v.source}:{v.mode}'.format(v=self) + def parse_restart_spec(restart_config): if not restart_config: @@ -156,3 +159,7 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): mode = parts[2] return cls(external, internal, mode) + + def repr(self): + external = self.external + ':' if self.external else '' + return '{ext}{v.internal}:{v.mode}'.format(ext=external, v=self) diff --git a/compose/network.py b/compose/network.py index b2ba2e9b7..eaad770cf 100644 --- a/compose/network.py +++ b/compose/network.py @@ -65,7 +65,10 @@ class Network(object): ) def remove(self): - # TODO: don't remove external networks + if self.external_name: + log.info("Network %s is external, skipping", self.full_name) + return + log.info("Removing network {}".format(self.full_name)) self.client.remove_network(self.full_name) diff --git a/compose/project.py b/compose/project.py index 12d52cc26..1322c9902 100644 --- a/compose/project.py +++ b/compose/project.py @@ -275,7 +275,7 @@ class Project(object): def down(self, remove_image_type, include_volumes): self.stop() self.remove_stopped(v=include_volumes) - self.remove_default_network() + self.remove_networks() if include_volumes: self.remove_volumes() @@ -286,11 +286,11 @@ class Project(object): for service in self.get_services(): service.remove_image(remove_image_type) - def remove_default_network(self): + def remove_networks(self): if not self.use_networking: return - if self.uses_default_network(): - self.default_network.remove() + for network in self.networks: + network.remove() def remove_volumes(self): for volume in self.volumes: diff --git a/compose/service.py b/compose/service.py index c91c3a58c..0866b83bb 100644 --- a/compose/service.py +++ b/compose/service.py @@ -460,7 +460,8 @@ class Service(object): 'links': self.get_link_names(), 'net': self.net.id, 'volumes_from': [ - (v.source.name, v.mode) for v in self.volumes_from if isinstance(v.source, Service) + (v.source.name, v.mode) + for v in self.volumes_from if isinstance(v.source, Service) ], } @@ -519,12 +520,7 @@ class Service(object): return links def _get_volumes_from(self): - volumes_from = [] - for volume_from_spec in self.volumes_from: - volumes = build_volume_from(volume_from_spec) - volumes_from.extend(volumes) - - return volumes_from + return [build_volume_from(spec) for spec in self.volumes_from] def _get_container_create_options( self, @@ -927,7 +923,7 @@ def warn_on_masked_volume(volumes_option, container_volumes, service): def build_volume_binding(volume_spec): - return volume_spec.internal, "{}:{}:{}".format(*volume_spec) + return volume_spec.internal, volume_spec.repr() def build_volume_from(volume_from_spec): @@ -938,12 +934,14 @@ def build_volume_from(volume_from_spec): if isinstance(volume_from_spec.source, Service): containers = volume_from_spec.source.containers(stopped=True) if not containers: - return ["{}:{}".format(volume_from_spec.source.create_container().id, volume_from_spec.mode)] + return "{}:{}".format( + volume_from_spec.source.create_container().id, + volume_from_spec.mode) container = containers[0] - return ["{}:{}".format(container.id, volume_from_spec.mode)] + return "{}:{}".format(container.id, volume_from_spec.mode) elif isinstance(volume_from_spec.source, Container): - return ["{}:{}".format(volume_from_spec.source.id, volume_from_spec.mode)] + return "{}:{}".format(volume_from_spec.source.id, volume_from_spec.mode) # Labels diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 9c9ced8ce..d910473a8 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -10,8 +10,8 @@ import subprocess import time from collections import namedtuple from operator import attrgetter -from textwrap import dedent +import yaml from docker import errors from .. import mock @@ -148,8 +148,9 @@ class CLITestCase(DockerClientTestCase): self.base_dir = None def test_config_list_services(self): + self.base_dir = 'tests/fixtures/v2-full' result = self.dispatch(['config', '--services']) - assert set(result.stdout.rstrip().split('\n')) == {'simple', 'another'} + assert set(result.stdout.rstrip().split('\n')) == {'web', 'other'} def test_config_quiet_with_error(self): self.base_dir = None @@ -160,20 +161,36 @@ class CLITestCase(DockerClientTestCase): assert "'notaservice' doesn't have any configuration" in result.stderr def test_config_quiet(self): + self.base_dir = 'tests/fixtures/v2-full' assert self.dispatch(['config', '-q']).stdout == '' def test_config_default(self): + self.base_dir = 'tests/fixtures/v2-full' result = self.dispatch(['config']) - assert dedent(""" - simple: - command: top - image: busybox:latest - """).lstrip() in result.stdout - assert dedent(""" - another: - command: top - image: busybox:latest - """).lstrip() in result.stdout + # assert there are no python objects encoded in the output + assert '!!' not in result.stdout + + output = yaml.load(result.stdout) + expected = { + 'version': 2, + 'volumes': {'data': {'driver': 'local'}}, + 'networks': {'front': {}}, + 'services': { + 'web': { + 'build': { + 'context': os.path.abspath(self.base_dir), + }, + 'networks': ['front', 'default'], + 'volumes_from': ['service:other:rw'], + }, + 'other': { + 'image': 'busybox:latest', + 'command': 'top', + 'volumes': ['/data:rw'], + }, + }, + } + assert output == expected def test_ps(self): self.project.get_service('simple').create_container() @@ -340,16 +357,20 @@ class CLITestCase(DockerClientTestCase): assert '--rmi flag must be' in result.stderr def test_down(self): - self.base_dir = 'tests/fixtures/shutdown' + self.base_dir = 'tests/fixtures/v2-full' self.dispatch(['up', '-d']) - wait_on_condition(ContainerCountCondition(self.project, 1)) + wait_on_condition(ContainerCountCondition(self.project, 2)) 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 + assert 'Stopping v2full_web_1' in result.stderr + assert 'Stopping v2full_other_1' in result.stderr + assert 'Removing v2full_web_1' in result.stderr + assert 'Removing v2full_other_1' in result.stderr + assert 'Removing volume v2full_data' in result.stderr + assert 'Removing image v2full_web' in result.stderr + assert 'Removing image busybox' not in result.stderr + assert 'Removing network v2full_default' in result.stderr + assert 'Removing network v2full_front' in result.stderr def test_up_detached(self): self.dispatch(['up', '-d']) diff --git a/tests/fixtures/shutdown/docker-compose.yml b/tests/fixtures/shutdown/docker-compose.yml deleted file mode 100644 index c83c3d637..000000000 --- a/tests/fixtures/shutdown/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ - -version: 2 - -volumes: - data: - driver: local - -services: - web: - build: . diff --git a/tests/fixtures/shutdown/Dockerfile b/tests/fixtures/v2-full/Dockerfile similarity index 100% rename from tests/fixtures/shutdown/Dockerfile rename to tests/fixtures/v2-full/Dockerfile diff --git a/tests/fixtures/v2-full/docker-compose.yml b/tests/fixtures/v2-full/docker-compose.yml new file mode 100644 index 000000000..725296c99 --- /dev/null +++ b/tests/fixtures/v2-full/docker-compose.yml @@ -0,0 +1,24 @@ + +version: 2 + +volumes: + data: + driver: local + +networks: + front: {} + +services: + web: + build: . + networks: + - front + - default + volumes_from: + - other + + other: + image: busybox:latest + command: top + volumes: + - /data