Implement smart recreate behind an experimental CLI flag
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
This commit is contained in:
parent
82bc7cd5ba
commit
ef4eb66723
11 changed files with 563 additions and 90 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
263
tests/integration/state_test.py
Normal file
263
tests/integration/state_test.py
Normal 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)
|
||||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue