From ef40e3c6b99e24c580eabd57580778d60ae79d99 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 19 Apr 2017 16:47:43 -0700 Subject: [PATCH] Prevent `docker-compose scale` to be used with a v2.2 config file Signed-off-by: Joffrey F --- compose/cli/main.py | 7 ++++++ compose/project.py | 5 +++-- compose/service.py | 13 +++++------ tests/acceptance/cli_test.py | 36 ++++++++++++++++++++++++++----- tests/integration/project_test.py | 6 +++--- tests/integration/service_test.py | 18 +++++++++------- 6 files changed, 61 insertions(+), 24 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index e018a0174..0fdf3c28a 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -26,6 +26,7 @@ from ..config import resolve_build_args from ..config.environment import Environment from ..config.serialize import serialize_config from ..config.types import VolumeSpec +from ..const import COMPOSEFILE_V2_2 as V2_2 from ..const import IS_WINDOWS_PLATFORM from ..errors import StreamParseError from ..progress_stream import StreamOutputError @@ -771,6 +772,12 @@ class TopLevelCommand(object): """ timeout = timeout_from_opts(options) + if self.project.config_version == V2_2: + raise UserError( + 'The scale command is incompatible with the v2.2 format. ' + 'Use the up command with the --scale flag instead.' + ) + for service_name, num in parse_scale_args(options['SERVICE=NUM']).items(): self.project.get_service(service_name).scale(num, timeout=timeout) diff --git a/compose/project.py b/compose/project.py index 853228764..e80b10455 100644 --- a/compose/project.py +++ b/compose/project.py @@ -57,12 +57,13 @@ class Project(object): """ A collection of services. """ - def __init__(self, name, services, client, networks=None, volumes=None): + def __init__(self, name, services, client, networks=None, volumes=None, config_version=None): self.name = name self.services = services self.client = client self.volumes = volumes or ProjectVolumes({}) self.networks = networks or ProjectNetworks({}, False) + self.config_version = config_version def labels(self, one_off=OneOffFilter.exclude): labels = ['{0}={1}'.format(LABEL_PROJECT, self.name)] @@ -82,7 +83,7 @@ class Project(object): networks, use_networking) volumes = ProjectVolumes.from_config(name, config_data, client) - project = cls(name, [], client, project_networks, volumes) + project = cls(name, [], client, project_networks, volumes, config_data.version) for service_dict in config_data.services: service_dict = dict(service_dict) diff --git a/compose/service.py b/compose/service.py index 65eded8ec..e903115af 100644 --- a/compose/service.py +++ b/compose/service.py @@ -383,8 +383,8 @@ class Service(object): lambda n: self.get_container_name(n), "Creating" ) - if errors: - raise OperationFailedError(errors.values()[0]) + for error in errors.values(): + raise OperationFailedError(error) return containers @@ -404,8 +404,9 @@ class Service(object): lambda c: c.name, "Recreating" ) - if errors: - raise OperationFailedError(errors.values()[0]) + for error in errors.values(): + raise OperationFailedError(error) + if len(containers) < scale: containers.extend(self._execute_convergence_create( scale - len(containers), detached, start @@ -424,8 +425,8 @@ class Service(object): "Starting" ) - if errors: - raise OperationFailedError(errors.values()[0]) + for error in errors.values(): + raise OperationFailedError(error) if len(containers) < scale: containers.extend(self._execute_convergence_create( diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index c4b24b4b5..75b15ae65 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1866,9 +1866,27 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(project.get_service('simple').containers()), 0) self.assertEqual(len(project.get_service('another').containers()), 0) - def test_up_scale(self): + def test_scale_v2_2(self): + self.base_dir = 'tests/fixtures/scale' + result = self.dispatch(['scale', 'web=1'], returncode=1) + assert 'incompatible with the v2.2 format' in result.stderr + + def test_up_scale_scale_up(self): self.base_dir = 'tests/fixtures/scale' project = self.project + + self.dispatch(['up', '-d']) + assert len(project.get_service('web').containers()) == 2 + assert len(project.get_service('db').containers()) == 1 + + self.dispatch(['up', '-d', '--scale', 'web=3']) + assert len(project.get_service('web').containers()) == 3 + assert len(project.get_service('db').containers()) == 1 + + def test_up_scale_scale_down(self): + self.base_dir = 'tests/fixtures/scale' + project = self.project + self.dispatch(['up', '-d']) assert len(project.get_service('web').containers()) == 2 assert len(project.get_service('db').containers()) == 1 @@ -1877,13 +1895,21 @@ class CLITestCase(DockerClientTestCase): assert len(project.get_service('web').containers()) == 1 assert len(project.get_service('db').containers()) == 1 - self.dispatch(['up', '-d', '--scale', 'web=3']) + def test_up_scale_reset(self): + self.base_dir = 'tests/fixtures/scale' + project = self.project + + self.dispatch(['up', '-d', '--scale', 'web=3', '--scale', 'db=3']) assert len(project.get_service('web').containers()) == 3 + assert len(project.get_service('db').containers()) == 3 + + self.dispatch(['up', '-d']) + assert len(project.get_service('web').containers()) == 2 assert len(project.get_service('db').containers()) == 1 - self.dispatch(['up', '-d', '--scale', 'web=1', '--scale', 'db=2']) - assert len(project.get_service('web').containers()) == 1 - assert len(project.get_service('db').containers()) == 2 + def test_up_scale_to_zero(self): + self.base_dir = 'tests/fixtures/scale' + project = self.project self.dispatch(['up', '-d']) assert len(project.get_service('web').containers()) == 2 diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index afb408c83..b69b04565 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -565,12 +565,12 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(len(service.containers()), 3) project.up() service = project.get_service('web') - self.assertEqual(len(service.containers()), 3) + self.assertEqual(len(service.containers()), 1) service.scale(1) self.assertEqual(len(service.containers()), 1) - project.up() + project.up(scale_override={'web': 3}) service = project.get_service('web') - self.assertEqual(len(service.containers()), 1) + self.assertEqual(len(service.containers()), 3) # does scale=0 ,makes any sense? after recreating at least 1 container is running service.scale(0) project.up() diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 636071755..87549c506 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -26,6 +26,7 @@ from compose.const import LABEL_PROJECT from compose.const import LABEL_SERVICE from compose.const import LABEL_VERSION from compose.container import Container +from compose.errors import OperationFailedError from compose.project import OneOffFilter from compose.service import ConvergencePlan from compose.service import ConvergenceStrategy @@ -777,15 +778,15 @@ class ServiceTest(DockerClientTestCase): message="testing", response={}, explanation="Boom")): - with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr: - service.scale(3) + with pytest.raises(OperationFailedError): + service.scale(3) - self.assertEqual(len(service.containers()), 1) - self.assertTrue(service.containers()[0].is_running) - self.assertIn( - "ERROR: for composetest_web_2 Cannot create container for service web: Boom", - mock_stderr.getvalue() + assert len(service.containers()) == 1 + assert service.containers()[0].is_running + assert ( + "ERROR: for composetest_web_2 Cannot create container for service" + " web: Boom" in mock_stderr.getvalue() ) def test_scale_with_unexpected_exception(self): @@ -837,7 +838,8 @@ class ServiceTest(DockerClientTestCase): service = self.create_service('app', container_name='custom-container') self.assertEqual(service.custom_container_name, 'custom-container') - service.scale(3) + with pytest.raises(OperationFailedError): + service.scale(3) captured_output = mock_log.warn.call_args[0][0]