diff --git a/docs/yml.md b/docs/yml.md index a911e450b86..b7a3e3b03cf 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -199,3 +199,12 @@ privileged: true restart: always ``` + +### initial_scale + +Specify inital scale for container. Upon service creation or recreation, `initial_scale` +instances of the service container will be created (and started). + +``` +initial_scale: 3 +``` diff --git a/fig/service.py b/fig/service.py index d06d271f682..b1a001d9ac6 100644 --- a/fig/service.py +++ b/fig/service.py @@ -93,7 +93,7 @@ def __init__(self, name, client=None, project='default', links=None, volumes_fro if 'image' in options and 'build' in options: raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) - supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose'] + supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose', 'initial_scale'] for k in options: if k not in supported_options: @@ -102,6 +102,10 @@ def __init__(self, name, client=None, project='default', links=None, volumes_fro msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k] raise ConfigError(msg) + if options.get('initial_scale', 1) > 1: + if not can_be_scaled(options): + raise CannotBeScaledError() + self.name = name self.client = client self.project = project @@ -164,7 +168,7 @@ def scale(self, desired_num): - starts containers until there are at least `desired_num` running - removes all stopped containers """ - if not self.can_be_scaled(): + if not can_be_scaled(self.options): raise CannotBeScaledError() # Create enough containers @@ -243,13 +247,12 @@ def recreate_containers(self, insecure_registry=False, do_build=True, **override """ containers = self.containers(stopped=True) if not containers: - log.info("Creating %s..." % self._next_container_name(containers)) - container = self.create_container( - insecure_registry=insecure_registry, - do_build=do_build, - **override_options) - self.start_container(container) - return [(None, container)] + new_containers = [] + for _ in range(self.options.get("initial_scale", 1)): + log.info("Creating %s..." % self._next_container_name(new_containers)) + new_container = self.create_container(insecure_registry=insecure_registry, do_build=do_build, **override_options) + new_containers.append(new_container) + return [(None, self.start_container(nc)) for nc in new_containers] else: tuples = [] @@ -341,13 +344,12 @@ def start_or_create_containers( containers = self.containers(stopped=True) if not containers: - log.info("Creating %s..." % self._next_container_name(containers)) - new_container = self.create_container( - insecure_registry=insecure_registry, - detach=detach, - do_build=do_build, - ) - return [self.start_container(new_container)] + new_containers = [] + for _ in range(self.options.get("initial_scale", 1)): + log.info("Creating %s..." % self._next_container_name(new_containers)) + new_container = self.create_container(insecure_registry=insecure_registry, detach=detach, do_build=do_build) + new_containers.append(new_container) + return [self.start_container(nc) for nc in new_containers] else: return [self.start_container_if_stopped(c) for c in containers] @@ -493,12 +495,6 @@ def full_name(self): """ return '%s_%s' % (self.project, self.name) - def can_be_scaled(self): - for port in self.options.get('ports', []): - if ':' in str(port): - return False - return True - def pull(self, insecure_registry=False): if 'image' in self.options: image_name = self._get_image_name(self.options['image']) @@ -665,3 +661,10 @@ def env_vars_from_file(filename): k, v = split_env(line) env[k] = v return env + + +def can_be_scaled(options): + for port in options.get('ports', []): + if ':' in str(port): + return False + return True diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 68dcf06ab46..c38b41ec2cc 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from __future__ import absolute_import import os +import functools from .. import unittest import mock @@ -18,6 +19,7 @@ build_volume_binding, APIError, parse_repository_tag, + CannotBeScaledError, ) @@ -245,6 +247,37 @@ def test_create_container_no_build(self): self.assertFalse(self.mock_client.images.called) self.assertFalse(self.mock_client.build.called) + def test_initial_scale_recreate(self): + def _side_effect(nd, *args, **kwargs): + nd['num'] = num = nd.get('num', 0) + 1 + return {'Id': 'aabbccdd%d' % num, 'Name': '/default_foo_%d' % num} + mock_client = mock.create_autospec(docker.Client) + mock_client.create_container.side_effect = functools.partial(_side_effect, {}) + mock_client.inspect_container.side_effect = functools.partial(_side_effect, {}) + service = Service('foo', image='redis', client=mock_client, initial_scale=3) + ret = service.recreate_containers() + self.assertEqual(3, len(ret)) + for intermediate, new in ret: + self.assertIsNone(intermediate) + self.assertIsInstance(new, Container) + + def test_initial_scale_start_or_create(self): + def _side_effect(nd, *args, **kwargs): + nd['num'] = num = nd.get('num', 0) + 1 + return {'Id': 'aabbccdd%d' % num, 'Name': '/default_foo_%d' % num} + mock_client = mock.create_autospec(docker.Client) + mock_client.create_container.side_effect = functools.partial(_side_effect, {}) + mock_client.inspect_container.side_effect = functools.partial(_side_effect, {}) + service = Service('foo', image='redis', client=mock_client, initial_scale=3) + ret = service.start_or_create_containers() + self.assertEqual(3, len(ret)) + for c in ret: + self.assertIsInstance(c, Container) + + def test_initial_scale_cannot_be_scaled(self): + with self.assertRaises(CannotBeScaledError): + service = Service('foo', client=self.mock_client, initial_scale=3, ports=["80:80"]) + class ServiceVolumesTest(unittest.TestCase):