state_test.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. """
  2. Integration tests which cover state convergence (aka smart recreate) performed
  3. by `docker-compose up`.
  4. """
  5. from __future__ import unicode_literals
  6. import os
  7. import shutil
  8. import tempfile
  9. from .testcases import DockerClientTestCase
  10. from compose.config import config
  11. from compose.project import Project
  12. from compose.service import ConvergenceStrategy
  13. class ProjectTestCase(DockerClientTestCase):
  14. def run_up(self, cfg, **kwargs):
  15. kwargs.setdefault('timeout', 1)
  16. kwargs.setdefault('detached', True)
  17. project = self.make_project(cfg)
  18. project.up(**kwargs)
  19. return set(project.containers(stopped=True))
  20. def make_project(self, cfg):
  21. details = config.ConfigDetails(
  22. 'working_dir',
  23. [config.ConfigFile(None, cfg)])
  24. return Project.from_dicts(
  25. name='composetest',
  26. client=self.client,
  27. service_dicts=config.load(details))
  28. class BasicProjectTest(ProjectTestCase):
  29. def setUp(self):
  30. super(BasicProjectTest, self).setUp()
  31. self.cfg = {
  32. 'db': {'image': 'busybox:latest'},
  33. 'web': {'image': 'busybox:latest'},
  34. }
  35. def test_no_change(self):
  36. old_containers = self.run_up(self.cfg)
  37. self.assertEqual(len(old_containers), 2)
  38. new_containers = self.run_up(self.cfg)
  39. self.assertEqual(len(new_containers), 2)
  40. self.assertEqual(old_containers, new_containers)
  41. def test_partial_change(self):
  42. old_containers = self.run_up(self.cfg)
  43. old_db = [c for c in old_containers if c.name_without_project == 'db_1'][0]
  44. old_web = [c for c in old_containers if c.name_without_project == 'web_1'][0]
  45. self.cfg['web']['command'] = '/bin/true'
  46. new_containers = self.run_up(self.cfg)
  47. self.assertEqual(len(new_containers), 2)
  48. preserved = list(old_containers & new_containers)
  49. self.assertEqual(preserved, [old_db])
  50. removed = list(old_containers - new_containers)
  51. self.assertEqual(removed, [old_web])
  52. created = list(new_containers - old_containers)
  53. self.assertEqual(len(created), 1)
  54. self.assertEqual(created[0].name_without_project, 'web_1')
  55. self.assertEqual(created[0].get('Config.Cmd'), ['/bin/true'])
  56. def test_all_change(self):
  57. old_containers = self.run_up(self.cfg)
  58. self.assertEqual(len(old_containers), 2)
  59. self.cfg['web']['command'] = '/bin/true'
  60. self.cfg['db']['command'] = '/bin/true'
  61. new_containers = self.run_up(self.cfg)
  62. self.assertEqual(len(new_containers), 2)
  63. unchanged = old_containers & new_containers
  64. self.assertEqual(len(unchanged), 0)
  65. new = new_containers - old_containers
  66. self.assertEqual(len(new), 2)
  67. class ProjectWithDependenciesTest(ProjectTestCase):
  68. def setUp(self):
  69. super(ProjectWithDependenciesTest, self).setUp()
  70. self.cfg = {
  71. 'db': {
  72. 'image': 'busybox:latest',
  73. 'command': 'tail -f /dev/null',
  74. },
  75. 'web': {
  76. 'image': 'busybox:latest',
  77. 'command': 'tail -f /dev/null',
  78. 'links': ['db'],
  79. },
  80. 'nginx': {
  81. 'image': 'busybox:latest',
  82. 'command': 'tail -f /dev/null',
  83. 'links': ['web'],
  84. },
  85. }
  86. def test_up(self):
  87. containers = self.run_up(self.cfg)
  88. self.assertEqual(
  89. set(c.name_without_project for c in containers),
  90. set(['db_1', 'web_1', 'nginx_1']),
  91. )
  92. def test_change_leaf(self):
  93. old_containers = self.run_up(self.cfg)
  94. self.cfg['nginx']['environment'] = {'NEW_VAR': '1'}
  95. new_containers = self.run_up(self.cfg)
  96. self.assertEqual(
  97. set(c.name_without_project for c in new_containers - old_containers),
  98. set(['nginx_1']),
  99. )
  100. def test_change_middle(self):
  101. old_containers = self.run_up(self.cfg)
  102. self.cfg['web']['environment'] = {'NEW_VAR': '1'}
  103. new_containers = self.run_up(self.cfg)
  104. self.assertEqual(
  105. set(c.name_without_project for c in new_containers - old_containers),
  106. set(['web_1', 'nginx_1']),
  107. )
  108. def test_change_root(self):
  109. old_containers = self.run_up(self.cfg)
  110. self.cfg['db']['environment'] = {'NEW_VAR': '1'}
  111. new_containers = self.run_up(self.cfg)
  112. self.assertEqual(
  113. set(c.name_without_project for c in new_containers - old_containers),
  114. set(['db_1', 'web_1', 'nginx_1']),
  115. )
  116. def test_change_root_no_recreate(self):
  117. old_containers = self.run_up(self.cfg)
  118. self.cfg['db']['environment'] = {'NEW_VAR': '1'}
  119. new_containers = self.run_up(
  120. self.cfg,
  121. strategy=ConvergenceStrategy.never)
  122. self.assertEqual(new_containers - old_containers, set())
  123. def test_service_removed_while_down(self):
  124. next_cfg = {
  125. 'web': {
  126. 'image': 'busybox:latest',
  127. 'command': 'tail -f /dev/null',
  128. },
  129. 'nginx': self.cfg['nginx'],
  130. }
  131. containers = self.run_up(self.cfg)
  132. self.assertEqual(len(containers), 3)
  133. project = self.make_project(self.cfg)
  134. project.stop(timeout=1)
  135. containers = self.run_up(next_cfg)
  136. self.assertEqual(len(containers), 2)
  137. class ServiceStateTest(DockerClientTestCase):
  138. """Test cases for Service.convergence_plan."""
  139. def test_trigger_create(self):
  140. web = self.create_service('web')
  141. self.assertEqual(('create', []), web.convergence_plan())
  142. def test_trigger_noop(self):
  143. web = self.create_service('web')
  144. container = web.create_container()
  145. web.start()
  146. web = self.create_service('web')
  147. self.assertEqual(('noop', [container]), web.convergence_plan())
  148. def test_trigger_start(self):
  149. options = dict(command=["top"])
  150. web = self.create_service('web', **options)
  151. web.scale(2)
  152. containers = web.containers(stopped=True)
  153. containers[0].stop()
  154. containers[0].inspect()
  155. self.assertEqual([c.is_running for c in containers], [False, True])
  156. self.assertEqual(
  157. ('start', containers[0:1]),
  158. web.convergence_plan(),
  159. )
  160. def test_trigger_recreate_with_config_change(self):
  161. web = self.create_service('web', command=["top"])
  162. container = web.create_container()
  163. web = self.create_service('web', command=["top", "-d", "1"])
  164. self.assertEqual(('recreate', [container]), web.convergence_plan())
  165. def test_trigger_recreate_with_nonexistent_image_tag(self):
  166. web = self.create_service('web', image="busybox:latest")
  167. container = web.create_container()
  168. web = self.create_service('web', image="nonexistent-image")
  169. self.assertEqual(('recreate', [container]), web.convergence_plan())
  170. def test_trigger_recreate_with_image_change(self):
  171. repo = 'composetest_myimage'
  172. tag = 'latest'
  173. image = '{}:{}'.format(repo, tag)
  174. image_id = self.client.images(name='busybox')[0]['Id']
  175. self.client.tag(image_id, repository=repo, tag=tag)
  176. try:
  177. web = self.create_service('web', image=image)
  178. container = web.create_container()
  179. # update the image
  180. c = self.client.create_container(image, ['touch', '/hello.txt'])
  181. self.client.commit(c, repository=repo, tag=tag)
  182. self.client.remove_container(c)
  183. web = self.create_service('web', image=image)
  184. self.assertEqual(('recreate', [container]), web.convergence_plan())
  185. finally:
  186. self.client.remove_image(image)
  187. def test_trigger_recreate_with_build(self):
  188. context = tempfile.mkdtemp()
  189. base_image = "FROM busybox\nLABEL com.docker.compose.test_image=true\n"
  190. try:
  191. dockerfile = os.path.join(context, 'Dockerfile')
  192. with open(dockerfile, 'w') as f:
  193. f.write(base_image)
  194. web = self.create_service('web', build=context)
  195. container = web.create_container()
  196. with open(dockerfile, 'w') as f:
  197. f.write(base_image + 'CMD echo hello world\n')
  198. web.build()
  199. web = self.create_service('web', build=context)
  200. self.assertEqual(('recreate', [container]), web.convergence_plan())
  201. finally:
  202. shutil.rmtree(context)