Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
47 changes: 25 additions & 22 deletions fig/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = []

Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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'])
Expand Down Expand Up @@ -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
33 changes: 33 additions & 0 deletions tests/unit/service_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import os
import functools

from .. import unittest
import mock
Expand All @@ -18,6 +19,7 @@
build_volume_binding,
APIError,
parse_repository_tag,
CannotBeScaledError,
)


Expand Down Expand Up @@ -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):

Expand Down