From 0f234154c24da87d524e39255e659ae340227278 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 14 Jan 2016 14:35:02 -0500 Subject: [PATCH 1/3] Remove all non-external networks on down. Also moves the shutdown test fixtures to be a more general v2-full fixture. Signed-off-by: Daniel Nephin --- compose/network.py | 5 ++++- compose/project.py | 8 ++++---- tests/acceptance/cli_test.py | 18 ++++++++++------- tests/fixtures/shutdown/docker-compose.yml | 10 ---------- .../fixtures/{shutdown => v2-full}/Dockerfile | 0 tests/fixtures/v2-full/docker-compose.yml | 20 +++++++++++++++++++ 6 files changed, 39 insertions(+), 22 deletions(-) delete mode 100644 tests/fixtures/shutdown/docker-compose.yml rename tests/fixtures/{shutdown => v2-full}/Dockerfile (100%) create mode 100644 tests/fixtures/v2-full/docker-compose.yml 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/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 9c9ced8ce..548c6b939 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -340,16 +340,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..86d1c2c22 --- /dev/null +++ b/tests/fixtures/v2-full/docker-compose.yml @@ -0,0 +1,20 @@ + +version: 2 + +volumes: + data: + driver: local + +networks: + front: {} + +services: + web: + build: . + networks: + - front + - default + + other: + image: busybox:latest + command: top From 3021ee12fe021092673930bd0ad578783a51dffa Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 14 Jan 2016 15:09:04 -0500 Subject: [PATCH 2/3] Fix `config` command to print the new sections of the config Signed-off-by: Daniel Nephin --- compose/cli/main.py | 2 +- tests/acceptance/cli_test.py | 37 +++++++++++++++-------- tests/fixtures/v2-full/docker-compose.yml | 4 +++ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index 473c6d605..661c91f2a 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -217,7 +217,7 @@ class TopLevelCommand(DocoptCommand): compose_config = dict( (service.pop('name'), service) for service in compose_config.services) - print(yaml.dump( + print(yaml.safe_dump( compose_config, default_flow_style=False, indent=2, diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 548c6b939..d93881997 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,32 @@ 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': os.path.abspath(self.base_dir), + 'networks': ['front', 'default'], + }, + 'other': { + 'image': 'busybox:latest', + 'command': 'top', + }, + }, + } + assert output == expected def test_ps(self): self.project.get_service('simple').create_container() diff --git a/tests/fixtures/v2-full/docker-compose.yml b/tests/fixtures/v2-full/docker-compose.yml index 86d1c2c22..725296c99 100644 --- a/tests/fixtures/v2-full/docker-compose.yml +++ b/tests/fixtures/v2-full/docker-compose.yml @@ -14,7 +14,11 @@ services: networks: - front - default + volumes_from: + - other other: image: busybox:latest command: top + volumes: + - /data From 1bfbba36b27df69302f3a196834d32d3dc64987e Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 14 Jan 2016 17:30:48 -0500 Subject: [PATCH 3/3] Ensure that the config output by config command never contains python objects. Signed-off-by: Daniel Nephin --- compose/cli/main.py | 10 ++-------- compose/config/serialize.py | 30 ++++++++++++++++++++++++++++++ compose/config/types.py | 7 +++++++ compose/service.py | 20 +++++++++----------- tests/acceptance/cli_test.py | 6 +++++- 5 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 compose/config/serialize.py diff --git a/compose/cli/main.py b/compose/cli/main.py index 661c91f2a..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.safe_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/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 d93881997..d910473a8 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -177,12 +177,16 @@ class CLITestCase(DockerClientTestCase): 'networks': {'front': {}}, 'services': { 'web': { - 'build': os.path.abspath(self.base_dir), + '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'], }, }, }