Implement smart recreate behind an experimental CLI flag

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
This commit is contained in:
Aanand Prasad 2015-05-12 11:11:36 +01:00
parent 82bc7cd5ba
commit ef4eb66723
11 changed files with 563 additions and 90 deletions

View File

@ -13,7 +13,7 @@ import dockerpty
from .. import __version__
from .. import migration
from ..project import NoSuchService, ConfigurationError
from ..service import BuildError, CannotBeScaledError
from ..service import BuildError, CannotBeScaledError, NeedsBuildError
from ..config import parse_environment
from .command import Command
from .docopt_command import NoSuchCommand
@ -47,6 +47,9 @@ def main():
except BuildError as e:
log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
sys.exit(1)
except NeedsBuildError as e:
log.error("Service '%s' needs to be built, but --no-build was passed." % e.service.name)
sys.exit(1)
def setup_logging():
@ -297,7 +300,7 @@ class TopLevelCommand(Command):
project.up(
service_names=deps,
start_deps=True,
recreate=False,
allow_recreate=False,
insecure_registry=insecure_registry,
)
@ -440,6 +443,8 @@ class TopLevelCommand(Command):
print new container names.
--no-color Produce monochrome output.
--no-deps Don't start linked services.
--x-smart-recreate Only recreate containers whose configuration or
image needs to be updated. (EXPERIMENTAL)
--no-recreate If containers already exist, don't recreate them.
--no-build Don't build an image, even if it's missing
-t, --timeout TIMEOUT When attached, use this timeout in seconds
@ -452,13 +457,15 @@ class TopLevelCommand(Command):
monochrome = options['--no-color']
start_deps = not options['--no-deps']
recreate = not options['--no-recreate']
allow_recreate = not options['--no-recreate']
smart_recreate = options['--x-smart-recreate']
service_names = options['SERVICE']
project.up(
service_names=service_names,
start_deps=start_deps,
recreate=recreate,
allow_recreate=allow_recreate,
smart_recreate=smart_recreate,
insecure_registry=insecure_registry,
do_build=not options['--no-build'],
)

View File

@ -4,3 +4,4 @@ LABEL_ONE_OFF = 'com.docker.compose.oneoff'
LABEL_PROJECT = 'com.docker.compose.project'
LABEL_SERVICE = 'com.docker.compose.service'
LABEL_VERSION = 'com.docker.compose.version'
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'

View File

@ -179,13 +179,16 @@ class Container(object):
return self.client.attach_socket(self.id, **kwargs)
def __repr__(self):
return '<Container: %s>' % self.name
return '<Container: %s (%s)>' % (self.name, self.id[:6])
def __eq__(self, other):
if type(self) != type(other):
return False
return self.id == other.id
def __hash__(self):
return self.id.__hash__()
def get_container_name(container):
if not container.get('Name') and not container.get('Names'):

View File

@ -207,22 +207,59 @@ class Project(object):
def up(self,
service_names=None,
start_deps=True,
recreate=True,
allow_recreate=True,
smart_recreate=False,
insecure_registry=False,
do_build=True):
running_containers = []
for service in self.get_services(service_names, include_deps=start_deps):
if recreate:
create_func = service.recreate_containers
services = self.get_services(service_names, include_deps=start_deps)
plans = self._get_convergence_plans(
services,
allow_recreate=allow_recreate,
smart_recreate=smart_recreate,
)
return [
container
for service in services
for container in service.execute_convergence_plan(
plans[service.name],
insecure_registry=insecure_registry,
do_build=do_build,
)
]
def _get_convergence_plans(self,
services,
allow_recreate=True,
smart_recreate=False):
plans = {}
for service in services:
updated_dependencies = [
name
for name in service.get_dependency_names()
if name in plans
and plans[name].action == 'recreate'
]
if updated_dependencies:
log.debug(
'%s has not changed but its dependencies (%s) have, so recreating',
service.name, ", ".join(updated_dependencies),
)
plan = service.recreate_plan()
else:
create_func = service.start_or_create_containers
plan = service.convergence_plan(
allow_recreate=allow_recreate,
smart_recreate=smart_recreate,
)
for container in create_func(
insecure_registry=insecure_registry,
do_build=do_build):
running_containers.append(container)
plans[service.name] = plan
return running_containers
return plans
def pull(self, service_names=None, insecure_registry=False):
for service in self.get_services(service_names, include_deps=True):
@ -250,10 +287,7 @@ class Project(object):
return containers
def _inject_deps(self, acc, service):
net_name = service.get_net_name()
dep_names = (service.get_linked_names() +
service.get_volumes_from_names() +
([net_name] if net_name else []))
dep_names = service.get_dependency_names()
if len(dep_names) > 0:
dep_services = self.get_services(

View File

@ -18,9 +18,11 @@ from .const import (
LABEL_PROJECT,
LABEL_SERVICE,
LABEL_VERSION,
LABEL_CONFIG_HASH,
)
from .container import Container, get_container_name
from .progress_stream import stream_output, StreamOutputError
from .utils import json_hash
log = logging.getLogger(__name__)
@ -59,12 +61,20 @@ class ConfigError(ValueError):
pass
class NeedsBuildError(Exception):
def __init__(self, service):
self.service = service
VolumeSpec = namedtuple('VolumeSpec', 'external internal mode')
ServiceName = namedtuple('ServiceName', 'project service number')
ConvergencePlan = namedtuple('ConvergencePlan', 'action containers')
class Service(object):
def __init__(self, name, client=None, project='default', links=None, external_links=None, volumes_from=None, net=None, **options):
if not re.match('^%s+$' % VALID_NAME_CHARS, name):
@ -192,6 +202,11 @@ class Service(object):
Create a container for this service. If the image doesn't exist, attempt to pull
it.
"""
self.ensure_image_exists(
do_build=do_build,
insecure_registry=insecure_registry,
)
container_options = self._get_container_create_options(
override_options,
number or self._next_container_number(one_off=one_off),
@ -199,38 +214,142 @@ class Service(object):
previous_container=previous_container,
)
if (do_build and
self.can_be_built() and
not self.client.images(name=self.full_name)):
self.build()
return Container.create(self.client, **container_options)
def ensure_image_exists(self,
do_build=True,
insecure_registry=False):
if self.image():
return
if self.can_be_built():
if do_build:
self.build()
else:
raise NeedsBuildError(self)
else:
self.pull(insecure_registry=insecure_registry)
def image(self):
try:
return Container.create(self.client, **container_options)
return self.client.inspect_image(self.image_name)
except APIError as e:
if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation):
self.pull(insecure_registry=insecure_registry)
return Container.create(self.client, **container_options)
raise
return None
else:
raise
def recreate_containers(self, insecure_registry=False, do_build=True):
@property
def image_name(self):
if self.can_be_built():
return self.full_name
else:
return self.options['image']
def converge(self,
allow_recreate=True,
smart_recreate=False,
insecure_registry=False,
do_build=True):
"""
If a container for this service doesn't exist, create and start one. If there are
any, stop them, create+start new ones, and remove the old containers.
"""
plan = self.convergence_plan(
allow_recreate=allow_recreate,
smart_recreate=smart_recreate,
)
return self.execute_convergence_plan(
plan,
insecure_registry=insecure_registry,
do_build=do_build,
)
def convergence_plan(self,
allow_recreate=True,
smart_recreate=False):
containers = self.containers(stopped=True)
if not containers:
return ConvergencePlan('create', [])
if smart_recreate and not self._containers_have_diverged(containers):
stopped = [c for c in containers if not c.is_running]
if stopped:
return ConvergencePlan('start', stopped)
return ConvergencePlan('noop', containers)
if not allow_recreate:
return ConvergencePlan('start', containers)
return ConvergencePlan('recreate', containers)
def recreate_plan(self):
containers = self.containers(stopped=True)
return ConvergencePlan('recreate', containers)
def _containers_have_diverged(self, containers):
config_hash = self.config_hash()
has_diverged = False
for c in containers:
container_config_hash = c.labels.get(LABEL_CONFIG_HASH, None)
if container_config_hash != config_hash:
log.debug(
'%s has diverged: %s != %s',
c.name, container_config_hash, config_hash,
)
has_diverged = True
return has_diverged
def execute_convergence_plan(self,
plan,
insecure_registry=False,
do_build=True):
(action, containers) = plan
if action == 'create':
container = self.create_container(
insecure_registry=insecure_registry,
do_build=do_build)
do_build=do_build,
)
self.start_container(container)
return [container]
return [
self.recreate_container(c, insecure_registry=insecure_registry)
for c in containers
]
elif action == 'recreate':
return [
self.recreate_container(
c,
insecure_registry=insecure_registry,
)
for c in containers
]
def recreate_container(self, container, insecure_registry=False):
elif action == 'start':
for c in containers:
self.start_container_if_stopped(c)
return containers
elif action == 'noop':
for c in containers:
log.info("%s is up-to-date" % c.name)
return containers
else:
raise Exception("Invalid action: {}".format(action))
def recreate_container(self,
container,
insecure_registry=False):
"""Recreate a container.
The original container is renamed to a temporary name so that data
@ -289,6 +408,21 @@ class Service(object):
else:
return [self.start_container_if_stopped(c) for c in containers]
def config_hash(self):
return json_hash(self.config_dict())
def config_dict(self):
return {
'options': self.options,
'image_id': self.image()['Id'],
}
def get_dependency_names(self):
net_name = self.get_net_name()
return (self.get_linked_names() +
self.get_volumes_from_names() +
([net_name] if net_name else []))
def get_linked_names(self):
return [s.name for (s, _) in self.links]
@ -376,6 +510,9 @@ class Service(object):
number,
one_off=False,
previous_container=None):
add_config_hash = (not one_off and not override_options)
container_options = dict(
(k, self.options[k])
for k in DOCKER_CONFIG_KEYS if k in self.options)
@ -383,6 +520,13 @@ class Service(object):
container_options['name'] = self.get_container_name(number, one_off)
if add_config_hash:
config_hash = self.config_hash()
if 'labels' not in container_options:
container_options['labels'] = {}
container_options['labels'][LABEL_CONFIG_HASH] = config_hash
log.debug("Added config hash: %s" % config_hash)
if 'detach' not in container_options:
container_options['detach'] = True
@ -493,7 +637,7 @@ class Service(object):
build_output = self.client.build(
path=path,
tag=self.full_name,
tag=self.image_name,
stream=True,
rm=True,
nocache=no_cache,

0
compose/state.py Normal file
View File

9
compose/utils.py Normal file
View File

@ -0,0 +1,9 @@
import json
import hashlib
def json_hash(obj):
dump = json.dumps(obj, sort_keys=True)
h = hashlib.sha256()
h.update(dump)
return h.hexdigest()

View File

@ -185,7 +185,7 @@ class ProjectTest(DockerClientTestCase):
old_db_id = project.containers()[0].id
db_volume_path = project.containers()[0].inspect()['Volumes']['/var/db']
project.up(recreate=False)
project.up(allow_recreate=False)
self.assertEqual(len(project.containers()), 2)
db_container = [c for c in project.containers() if 'db' in c.name][0]
@ -204,7 +204,7 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(len(project.containers()), 0)
project.up(['db'])
project.stop()
project.kill()
old_containers = project.containers(stopped=True)
@ -212,10 +212,11 @@ class ProjectTest(DockerClientTestCase):
old_db_id = old_containers[0].id
db_volume_path = old_containers[0].inspect()['Volumes']['/var/db']
project.up(recreate=False)
project.up(allow_recreate=False)
new_containers = project.containers(stopped=True)
self.assertEqual(len(new_containers), 2)
self.assertEqual([c.is_running for c in new_containers], [True, True])
db_container = [c for c in new_containers if 'db' in c.name][0]
self.assertEqual(db_container.id, old_db_id)

View File

@ -238,7 +238,7 @@ class ServiceTest(DockerClientTestCase):
self.assertIn(volume_container_2.id,
host_container.get('HostConfig.VolumesFrom'))
def test_recreate_containers(self):
def test_converge(self):
service = self.create_service(
'db',
environment={'FOO': '1'},
@ -258,7 +258,7 @@ class ServiceTest(DockerClientTestCase):
num_containers_before = len(self.client.containers(all=True))
service.options['environment']['FOO'] = '2'
new_container, = service.recreate_containers()
new_container = service.converge()[0]
self.assertEqual(new_container.get('Config.Entrypoint'), ['sleep'])
self.assertEqual(new_container.get('Config.Cmd'), ['300'])
@ -275,7 +275,7 @@ class ServiceTest(DockerClientTestCase):
self.client.inspect_container,
old_container.id)
def test_recreate_containers_when_containers_are_stopped(self):
def test_converge_when_containers_are_stopped(self):
service = self.create_service(
'db',
environment={'FOO': '1'},
@ -285,10 +285,10 @@ class ServiceTest(DockerClientTestCase):
)
service.create_container()
self.assertEqual(len(service.containers(stopped=True)), 1)
service.recreate_containers()
service.converge()
self.assertEqual(len(service.containers(stopped=True)), 1)
def test_recreate_containers_with_image_declared_volume(self):
def test_converge_with_image_declared_volume(self):
service = Service(
project='composetest',
name='db',
@ -300,7 +300,7 @@ class ServiceTest(DockerClientTestCase):
self.assertEqual(old_container.get('Volumes').keys(), ['/data'])
volume_path = old_container.get('Volumes')['/data']
new_container = service.recreate_containers()[0]
new_container = service.converge()[0]
self.assertEqual(new_container.get('Volumes').keys(), ['/data'])
self.assertEqual(new_container.get('Volumes')['/data'], volume_path)
@ -651,8 +651,19 @@ class ServiceTest(DockerClientTestCase):
expected = dict(labels_dict, **compose_labels)
service = self.create_service('web', labels=labels_dict)
labels = create_and_start_container(service).labels
self.assertEqual(labels, expected)
labels = create_and_start_container(service).labels.items()
for pair in expected.items():
self.assertIn(pair, labels)
service.kill()
service.remove_stopped()
labels_list = ["%s=%s" % pair for pair in labels_dict.items()]
service = self.create_service('web', labels=labels_list)
labels = create_and_start_container(service).labels.items()
for pair in expected.items():
self.assertIn(pair, labels)
def test_empty_labels(self):
labels_list = ['foo', 'bar']

View File

@ -0,0 +1,263 @@
from __future__ import unicode_literals
import tempfile
import shutil
import os
from compose import config
from compose.project import Project
from compose.const import LABEL_CONFIG_HASH
from .testcases import DockerClientTestCase
class ProjectTestCase(DockerClientTestCase):
def run_up(self, cfg, **kwargs):
if 'smart_recreate' not in kwargs:
kwargs['smart_recreate'] = True
project = self.make_project(cfg)
project.up(**kwargs)
return set(project.containers(stopped=True))
def make_project(self, cfg):
return Project.from_dicts(
name='composetest',
client=self.client,
service_dicts=config.from_dictionary(cfg),
)
class BasicProjectTest(ProjectTestCase):
def setUp(self):
super(BasicProjectTest, self).setUp()
self.cfg = {
'db': {'image': 'busybox:latest'},
'web': {'image': 'busybox:latest'},
}
def test_no_change(self):
old_containers = self.run_up(self.cfg)
self.assertEqual(len(old_containers), 2)
new_containers = self.run_up(self.cfg)
self.assertEqual(len(new_containers), 2)
self.assertEqual(old_containers, new_containers)
def test_partial_change(self):
old_containers = self.run_up(self.cfg)
old_db = [c for c in old_containers if c.name_without_project == 'db_1'][0]
old_web = [c for c in old_containers if c.name_without_project == 'web_1'][0]
self.cfg['web']['command'] = '/bin/true'
new_containers = self.run_up(self.cfg)
self.assertEqual(len(new_containers), 2)
preserved = list(old_containers & new_containers)
self.assertEqual(preserved, [old_db])
removed = list(old_containers - new_containers)
self.assertEqual(removed, [old_web])
created = list(new_containers - old_containers)
self.assertEqual(len(created), 1)
self.assertEqual(created[0].name_without_project, 'web_1')
self.assertEqual(created[0].get('Config.Cmd'), ['/bin/true'])
def test_all_change(self):
old_containers = self.run_up(self.cfg)
self.assertEqual(len(old_containers), 2)
self.cfg['web']['command'] = '/bin/true'
self.cfg['db']['command'] = '/bin/true'
new_containers = self.run_up(self.cfg)
self.assertEqual(len(new_containers), 2)
unchanged = old_containers & new_containers
self.assertEqual(len(unchanged), 0)
new = new_containers - old_containers
self.assertEqual(len(new), 2)
class ProjectWithDependenciesTest(ProjectTestCase):
def setUp(self):
super(ProjectWithDependenciesTest, self).setUp()
self.cfg = {
'db': {
'image': 'busybox:latest',
'command': 'tail -f /dev/null',
},
'web': {
'image': 'busybox:latest',
'command': 'tail -f /dev/null',
'links': ['db'],
},
'nginx': {
'image': 'busybox:latest',
'command': 'tail -f /dev/null',
'links': ['web'],
},
}
def test_up(self):
containers = self.run_up(self.cfg)
self.assertEqual(
set(c.name_without_project for c in containers),
set(['db_1', 'web_1', 'nginx_1']),
)
def test_change_leaf(self):
old_containers = self.run_up(self.cfg)
self.cfg['nginx']['environment'] = {'NEW_VAR': '1'}
new_containers = self.run_up(self.cfg)
self.assertEqual(
set(c.name_without_project for c in new_containers - old_containers),
set(['nginx_1']),
)
def test_change_middle(self):
old_containers = self.run_up(self.cfg)
self.cfg['web']['environment'] = {'NEW_VAR': '1'}
new_containers = self.run_up(self.cfg)
self.assertEqual(
set(c.name_without_project for c in new_containers - old_containers),
set(['web_1', 'nginx_1']),
)
def test_change_root(self):
old_containers = self.run_up(self.cfg)
self.cfg['db']['environment'] = {'NEW_VAR': '1'}
new_containers = self.run_up(self.cfg)
self.assertEqual(
set(c.name_without_project for c in new_containers - old_containers),
set(['db_1', 'web_1', 'nginx_1']),
)
def test_change_root_no_recreate(self):
old_containers = self.run_up(self.cfg)
self.cfg['db']['environment'] = {'NEW_VAR': '1'}
new_containers = self.run_up(self.cfg, allow_recreate=False)
self.assertEqual(new_containers - old_containers, set())
class ServiceStateTest(DockerClientTestCase):
def test_trigger_create(self):
web = self.create_service('web')
self.assertEqual(('create', []), web.convergence_plan(smart_recreate=True))
def test_trigger_noop(self):
web = self.create_service('web')
container = web.create_container()
web.start()
web = self.create_service('web')
self.assertEqual(('noop', [container]), web.convergence_plan(smart_recreate=True))
def test_trigger_start(self):
options = dict(command=["/bin/sleep", "300"])
web = self.create_service('web', **options)
web.scale(2)
containers = web.containers(stopped=True)
containers[0].stop()
containers[0].inspect()
self.assertEqual([c.is_running for c in containers], [False, True])
web = self.create_service('web', **options)
self.assertEqual(
('start', containers[0:1]),
web.convergence_plan(smart_recreate=True),
)
def test_trigger_recreate_with_config_change(self):
web = self.create_service('web', command=["/bin/sleep", "300"])
container = web.create_container()
web = self.create_service('web', command=["/bin/sleep", "400"])
self.assertEqual(('recreate', [container]), web.convergence_plan(smart_recreate=True))
def test_trigger_recreate_with_image_change(self):
repo = 'composetest_myimage'
tag = 'latest'
image = '{}:{}'.format(repo, tag)
image_id = self.client.images(name='busybox')[0]['Id']
self.client.tag(image_id, repository=repo, tag=tag)
try:
web = self.create_service('web', image=image)
container = web.create_container()
# update the image
c = self.client.create_container(image, ['touch', '/hello.txt'])
self.client.commit(c, repository=repo, tag=tag)
self.client.remove_container(c)
web = self.create_service('web', image=image)
self.assertEqual(('recreate', [container]), web.convergence_plan(smart_recreate=True))
finally:
self.client.remove_image(image)
def test_trigger_recreate_with_build(self):
context = tempfile.mkdtemp()
try:
dockerfile = os.path.join(context, 'Dockerfile')
with open(dockerfile, 'w') as f:
f.write('FROM busybox\n')
web = self.create_service('web', build=context)
container = web.create_container()
with open(dockerfile, 'w') as f:
f.write('FROM busybox\nCMD echo hello world\n')
web.build()
web = self.create_service('web', build=context)
self.assertEqual(('recreate', [container]), web.convergence_plan(smart_recreate=True))
finally:
shutil.rmtree(context)
class ConfigHashTest(DockerClientTestCase):
def test_no_config_hash_when_one_off(self):
web = self.create_service('web')
container = web.create_container(one_off=True)
self.assertNotIn(LABEL_CONFIG_HASH, container.labels)
def test_no_config_hash_when_overriding_options(self):
web = self.create_service('web')
container = web.create_container(environment={'FOO': '1'})
self.assertNotIn(LABEL_CONFIG_HASH, container.labels)
def test_config_hash_with_custom_labels(self):
web = self.create_service('web', labels={'foo': '1'})
container = web.converge()[0]
self.assertIn(LABEL_CONFIG_HASH, container.labels)
self.assertIn('foo', container.labels)
def test_config_hash_sticks_around(self):
web = self.create_service('web', command=["/bin/sleep", "300"])
container = web.converge()[0]
self.assertIn(LABEL_CONFIG_HASH, container.labels)
web = self.create_service('web', command=["/bin/sleep", "400"])
container = web.converge()[0]
self.assertIn(LABEL_CONFIG_HASH, container.labels)

View File

@ -5,14 +5,13 @@ from .. import unittest
import mock
import docker
from requests import Response
from compose.service import Service
from compose.container import Container
from compose.const import LABEL_SERVICE, LABEL_PROJECT, LABEL_ONE_OFF
from compose.service import (
APIError,
ConfigError,
NeedsBuildError,
build_port_bindings,
build_volume_binding,
get_container_data_volumes,
@ -223,36 +222,28 @@ class ServiceTest(unittest.TestCase):
insecure_registry=False,
stream=True)
@mock.patch('compose.service.Container', autospec=True)
@mock.patch('compose.service.log', autospec=True)
def test_create_container_from_insecure_registry(
self,
mock_log,
mock_container):
def test_create_container_from_insecure_registry(self):
service = Service('foo', client=self.mock_client, image='someimage:sometag')
mock_response = mock.Mock(Response)
mock_response.status_code = 404
mock_response.reason = "Not Found"
mock_container.create.side_effect = APIError(
'Mock error', mock_response, "No such image")
images = []
# We expect the APIError because our service requires a
# non-existent image.
with self.assertRaises(APIError):
service.create_container(insecure_registry=True)
def pull(repo, tag=None, insecure_registry=False, **kwargs):
self.assertEqual('someimage', repo)
self.assertEqual('sometag', tag)
self.assertTrue(insecure_registry)
images.append({'Id': 'abc123'})
return []
self.mock_client.pull.assert_called_once_with(
'someimage',
tag='sometag',
insecure_registry=True,
stream=True)
mock_log.info.assert_called_with(
'Pulling foo (someimage:sometag)...')
service.image = lambda: images[0] if images else None
self.mock_client.pull = pull
service.create_container(insecure_registry=True)
self.assertEqual(1, len(images))
@mock.patch('compose.service.Container', autospec=True)
def test_recreate_container(self, _):
mock_container = mock.create_autospec(Container)
service = Service('foo', client=self.mock_client, image='someimage')
service.image = lambda: {'Id': 'abc123'}
new_container = service.recreate_container(mock_container)
mock_container.stop.assert_called_once_with()
@ -273,36 +264,45 @@ class ServiceTest(unittest.TestCase):
@mock.patch('compose.service.Container', autospec=True)
def test_create_container_latest_is_used_when_no_tag_specified(self, mock_container):
mock_container.create.side_effect = APIError(
"oops",
mock.Mock(status_code=404),
"No such image")
service = Service('foo', client=self.mock_client, image='someimage')
with self.assertRaises(APIError):
service.create_container()
self.mock_client.pull.assert_called_once_with(
'someimage',
tag='latest',
insecure_registry=False,
stream=True)
images = []
def pull(repo, tag=None, **kwargs):
self.assertEqual('someimage', repo)
self.assertEqual('latest', tag)
images.append({'Id': 'abc123'})
return []
service.image = lambda: images[0] if images else None
self.mock_client.pull = pull
service.create_container()
self.assertEqual(1, len(images))
def test_create_container_with_build(self):
self.mock_client.images.return_value = []
service = Service('foo', client=self.mock_client, build='.')
service.build = mock.create_autospec(service.build)
service.create_container(do_build=True)
self.mock_client.images.assert_called_once_with(name=service.full_name)
service.build.assert_called_once_with()
images = []
service.image = lambda *args, **kwargs: images[0] if images else None
service.build = lambda: images.append({'Id': 'abc123'})
service.create_container(do_build=True)
self.assertEqual(1, len(images))
def test_create_container_no_build(self):
self.mock_client.images.return_value = []
service = Service('foo', client=self.mock_client, build='.')
service.create_container(do_build=False)
service.image = lambda: {'Id': 'abc123'}
self.assertFalse(self.mock_client.images.called)
service.create_container(do_build=False)
self.assertFalse(self.mock_client.build.called)
def test_create_container_no_build_but_needs_build(self):
service = Service('foo', client=self.mock_client, build='.')
service.image = lambda: None
with self.assertRaises(NeedsBuildError):
service.create_container(do_build=False)
class ServiceVolumesTest(unittest.TestCase):