state_test.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. """
  2. Integration tests which cover state convergence (aka smart recreate) performed
  3. by `docker-compose up`.
  4. """
  5. import copy
  6. import os
  7. import shutil
  8. import tempfile
  9. from docker.errors import ImageNotFound
  10. from ..helpers import BUSYBOX_IMAGE_WITH_TAG
  11. from .testcases import DockerClientTestCase
  12. from .testcases import get_links
  13. from .testcases import no_cluster
  14. from compose.config import config
  15. from compose.project import Project
  16. from compose.service import ConvergenceStrategy
  17. class ProjectTestCase(DockerClientTestCase):
  18. def run_up(self, cfg, **kwargs):
  19. kwargs.setdefault('timeout', 1)
  20. kwargs.setdefault('detached', True)
  21. project = self.make_project(cfg)
  22. project.up(**kwargs)
  23. return set(project.containers(stopped=True))
  24. def make_project(self, cfg):
  25. details = config.ConfigDetails(
  26. 'working_dir',
  27. [config.ConfigFile(None, cfg)])
  28. return Project.from_config(
  29. name='composetest',
  30. client=self.client,
  31. config_data=config.load(details))
  32. class BasicProjectTest(ProjectTestCase):
  33. def setUp(self):
  34. super(BasicProjectTest, self).setUp()
  35. self.cfg = {
  36. 'db': {'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top'},
  37. 'web': {'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top'},
  38. }
  39. def test_no_change(self):
  40. old_containers = self.run_up(self.cfg)
  41. assert len(old_containers) == 2
  42. new_containers = self.run_up(self.cfg)
  43. assert len(new_containers) == 2
  44. assert old_containers == new_containers
  45. def test_partial_change(self):
  46. old_containers = self.run_up(self.cfg)
  47. old_db = [c for c in old_containers if c.name_without_project.startswith('db_')][0]
  48. old_web = [c for c in old_containers if c.name_without_project.startswith('web_')][0]
  49. self.cfg['web']['command'] = '/bin/true'
  50. new_containers = self.run_up(self.cfg)
  51. assert len(new_containers) == 2
  52. preserved = list(old_containers & new_containers)
  53. assert preserved == [old_db]
  54. removed = list(old_containers - new_containers)
  55. assert removed == [old_web]
  56. created = list(new_containers - old_containers)
  57. assert len(created) == 1
  58. assert created[0].name_without_project == old_web.name_without_project
  59. assert created[0].get('Config.Cmd') == ['/bin/true']
  60. def test_all_change(self):
  61. old_containers = self.run_up(self.cfg)
  62. assert len(old_containers) == 2
  63. self.cfg['web']['command'] = '/bin/true'
  64. self.cfg['db']['command'] = '/bin/true'
  65. new_containers = self.run_up(self.cfg)
  66. assert len(new_containers) == 2
  67. unchanged = old_containers & new_containers
  68. assert len(unchanged) == 0
  69. new = new_containers - old_containers
  70. assert len(new) == 2
  71. class ProjectWithDependenciesTest(ProjectTestCase):
  72. def setUp(self):
  73. super(ProjectWithDependenciesTest, self).setUp()
  74. self.cfg = {
  75. 'db': {
  76. 'image': BUSYBOX_IMAGE_WITH_TAG,
  77. 'command': 'tail -f /dev/null',
  78. },
  79. 'web': {
  80. 'image': BUSYBOX_IMAGE_WITH_TAG,
  81. 'command': 'tail -f /dev/null',
  82. 'links': ['db'],
  83. },
  84. 'nginx': {
  85. 'image': BUSYBOX_IMAGE_WITH_TAG,
  86. 'command': 'tail -f /dev/null',
  87. 'links': ['web'],
  88. },
  89. }
  90. def test_up(self):
  91. containers = self.run_up(self.cfg)
  92. assert set(c.service for c in containers) == set(['db', 'web', 'nginx'])
  93. def test_change_leaf(self):
  94. old_containers = self.run_up(self.cfg)
  95. self.cfg['nginx']['environment'] = {'NEW_VAR': '1'}
  96. new_containers = self.run_up(self.cfg)
  97. assert set(c.service for c in new_containers - old_containers) == set(['nginx'])
  98. def test_change_middle(self):
  99. old_containers = self.run_up(self.cfg)
  100. self.cfg['web']['environment'] = {'NEW_VAR': '1'}
  101. new_containers = self.run_up(self.cfg)
  102. assert set(c.service for c in new_containers - old_containers) == set(['web'])
  103. def test_change_middle_always_recreate_deps(self):
  104. old_containers = self.run_up(self.cfg, always_recreate_deps=True)
  105. self.cfg['web']['environment'] = {'NEW_VAR': '1'}
  106. new_containers = self.run_up(self.cfg, always_recreate_deps=True)
  107. assert set(c.service for c in new_containers - old_containers) == {'web', 'nginx'}
  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. assert set(c.service for c in new_containers - old_containers) == set(['db'])
  113. def test_change_root_always_recreate_deps(self):
  114. old_containers = self.run_up(self.cfg, always_recreate_deps=True)
  115. self.cfg['db']['environment'] = {'NEW_VAR': '1'}
  116. new_containers = self.run_up(self.cfg, always_recreate_deps=True)
  117. assert set(c.service for c in new_containers - old_containers) == {
  118. 'db', 'web', 'nginx'
  119. }
  120. def test_change_root_no_recreate(self):
  121. old_containers = self.run_up(self.cfg)
  122. self.cfg['db']['environment'] = {'NEW_VAR': '1'}
  123. new_containers = self.run_up(
  124. self.cfg,
  125. strategy=ConvergenceStrategy.never)
  126. assert new_containers - old_containers == set()
  127. def test_service_removed_while_down(self):
  128. next_cfg = {
  129. 'web': {
  130. 'image': BUSYBOX_IMAGE_WITH_TAG,
  131. 'command': 'tail -f /dev/null',
  132. },
  133. 'nginx': self.cfg['nginx'],
  134. }
  135. containers = self.run_up(self.cfg)
  136. assert len(containers) == 3
  137. project = self.make_project(self.cfg)
  138. project.stop(timeout=1)
  139. containers = self.run_up(next_cfg)
  140. assert len(containers) == 2
  141. def test_service_recreated_when_dependency_created(self):
  142. containers = self.run_up(self.cfg, service_names=['web'], start_deps=False)
  143. assert len(containers) == 1
  144. containers = self.run_up(self.cfg)
  145. assert len(containers) == 3
  146. web, = [c for c in containers if c.service == 'web']
  147. nginx, = [c for c in containers if c.service == 'nginx']
  148. db, = [c for c in containers if c.service == 'db']
  149. assert set(get_links(web)) == {
  150. 'composetest_db_1',
  151. 'db',
  152. 'db_1',
  153. }
  154. assert set(get_links(nginx)) == {
  155. 'composetest_web_1',
  156. 'web',
  157. 'web_1',
  158. }
  159. class ProjectWithDependsOnDependenciesTest(ProjectTestCase):
  160. def setUp(self):
  161. super(ProjectWithDependsOnDependenciesTest, self).setUp()
  162. self.cfg = {
  163. 'version': '2',
  164. 'services': {
  165. 'db': {
  166. 'image': BUSYBOX_IMAGE_WITH_TAG,
  167. 'command': 'tail -f /dev/null',
  168. },
  169. 'web': {
  170. 'image': BUSYBOX_IMAGE_WITH_TAG,
  171. 'command': 'tail -f /dev/null',
  172. 'depends_on': ['db'],
  173. },
  174. 'nginx': {
  175. 'image': BUSYBOX_IMAGE_WITH_TAG,
  176. 'command': 'tail -f /dev/null',
  177. 'depends_on': ['web'],
  178. },
  179. }
  180. }
  181. def test_up(self):
  182. local_cfg = copy.deepcopy(self.cfg)
  183. containers = self.run_up(local_cfg)
  184. assert set(c.service for c in containers) == set(['db', 'web', 'nginx'])
  185. def test_change_leaf(self):
  186. local_cfg = copy.deepcopy(self.cfg)
  187. old_containers = self.run_up(local_cfg)
  188. local_cfg['services']['nginx']['environment'] = {'NEW_VAR': '1'}
  189. new_containers = self.run_up(local_cfg)
  190. assert set(c.service for c in new_containers - old_containers) == set(['nginx'])
  191. def test_change_middle(self):
  192. local_cfg = copy.deepcopy(self.cfg)
  193. old_containers = self.run_up(local_cfg)
  194. local_cfg['services']['web']['environment'] = {'NEW_VAR': '1'}
  195. new_containers = self.run_up(local_cfg)
  196. assert set(c.service for c in new_containers - old_containers) == set(['web'])
  197. def test_change_middle_always_recreate_deps(self):
  198. local_cfg = copy.deepcopy(self.cfg)
  199. old_containers = self.run_up(local_cfg, always_recreate_deps=True)
  200. local_cfg['services']['web']['environment'] = {'NEW_VAR': '1'}
  201. new_containers = self.run_up(local_cfg, always_recreate_deps=True)
  202. assert set(c.service for c in new_containers - old_containers) == set(['web', 'nginx'])
  203. def test_change_root(self):
  204. local_cfg = copy.deepcopy(self.cfg)
  205. old_containers = self.run_up(local_cfg)
  206. local_cfg['services']['db']['environment'] = {'NEW_VAR': '1'}
  207. new_containers = self.run_up(local_cfg)
  208. assert set(c.service for c in new_containers - old_containers) == set(['db'])
  209. def test_change_root_always_recreate_deps(self):
  210. local_cfg = copy.deepcopy(self.cfg)
  211. old_containers = self.run_up(local_cfg, always_recreate_deps=True)
  212. local_cfg['services']['db']['environment'] = {'NEW_VAR': '1'}
  213. new_containers = self.run_up(local_cfg, always_recreate_deps=True)
  214. assert set(c.service for c in new_containers - old_containers) == set(['db', 'web', 'nginx'])
  215. def test_change_root_no_recreate(self):
  216. local_cfg = copy.deepcopy(self.cfg)
  217. old_containers = self.run_up(local_cfg)
  218. local_cfg['services']['db']['environment'] = {'NEW_VAR': '1'}
  219. new_containers = self.run_up(
  220. local_cfg,
  221. strategy=ConvergenceStrategy.never)
  222. assert new_containers - old_containers == set()
  223. def test_service_removed_while_down(self):
  224. local_cfg = copy.deepcopy(self.cfg)
  225. next_cfg = copy.deepcopy(self.cfg)
  226. del next_cfg['services']['db']
  227. del next_cfg['services']['web']['depends_on']
  228. containers = self.run_up(local_cfg)
  229. assert set(c.service for c in containers) == set(['db', 'web', 'nginx'])
  230. project = self.make_project(local_cfg)
  231. project.stop(timeout=1)
  232. next_containers = self.run_up(next_cfg)
  233. assert set(c.service for c in next_containers) == set(['web', 'nginx'])
  234. def test_service_removed_while_up(self):
  235. local_cfg = copy.deepcopy(self.cfg)
  236. containers = self.run_up(local_cfg)
  237. assert set(c.service for c in containers) == set(['db', 'web', 'nginx'])
  238. del local_cfg['services']['db']
  239. del local_cfg['services']['web']['depends_on']
  240. containers = self.run_up(local_cfg)
  241. assert set(c.service for c in containers) == set(['web', 'nginx'])
  242. def test_dependency_removed(self):
  243. local_cfg = copy.deepcopy(self.cfg)
  244. next_cfg = copy.deepcopy(self.cfg)
  245. del next_cfg['services']['nginx']['depends_on']
  246. containers = self.run_up(local_cfg, service_names=['nginx'])
  247. assert set(c.service for c in containers) == set(['db', 'web', 'nginx'])
  248. project = self.make_project(local_cfg)
  249. project.stop(timeout=1)
  250. next_containers = self.run_up(next_cfg, service_names=['nginx'])
  251. assert set(c.service for c in next_containers if c.is_running) == set(['nginx'])
  252. def test_dependency_added(self):
  253. local_cfg = copy.deepcopy(self.cfg)
  254. del local_cfg['services']['nginx']['depends_on']
  255. containers = self.run_up(local_cfg, service_names=['nginx'])
  256. assert set(c.service for c in containers) == set(['nginx'])
  257. local_cfg['services']['nginx']['depends_on'] = ['db']
  258. containers = self.run_up(local_cfg, service_names=['nginx'])
  259. assert set(c.service for c in containers) == set(['nginx', 'db'])
  260. class ServiceStateTest(DockerClientTestCase):
  261. """Test cases for Service.convergence_plan."""
  262. def test_trigger_create(self):
  263. web = self.create_service('web')
  264. assert ('create', []) == web.convergence_plan()
  265. def test_trigger_noop(self):
  266. web = self.create_service('web')
  267. container = web.create_container()
  268. web.start()
  269. web = self.create_service('web')
  270. assert ('noop', [container]) == web.convergence_plan()
  271. def test_trigger_start(self):
  272. options = dict(command=["top"])
  273. web = self.create_service('web', **options)
  274. web.scale(2)
  275. containers = web.containers(stopped=True)
  276. containers[0].stop()
  277. containers[0].inspect()
  278. assert [c.is_running for c in containers] == [False, True]
  279. assert ('start', containers[0:1]) == web.convergence_plan()
  280. def test_trigger_recreate_with_config_change(self):
  281. web = self.create_service('web', command=["top"])
  282. container = web.create_container()
  283. web = self.create_service('web', command=["top", "-d", "1"])
  284. assert ('recreate', [container]) == web.convergence_plan()
  285. def test_trigger_recreate_with_nonexistent_image_tag(self):
  286. web = self.create_service('web', image=BUSYBOX_IMAGE_WITH_TAG)
  287. container = web.create_container()
  288. web = self.create_service('web', image="nonexistent-image")
  289. assert ('recreate', [container]) == web.convergence_plan()
  290. def test_trigger_recreate_with_image_change(self):
  291. repo = 'composetest_myimage'
  292. tag = 'latest'
  293. image = '{}:{}'.format(repo, tag)
  294. def safe_remove_image(image):
  295. try:
  296. self.client.remove_image(image)
  297. except ImageNotFound:
  298. pass
  299. image_id = self.client.images(name='busybox')[0]['Id']
  300. self.client.tag(image_id, repository=repo, tag=tag)
  301. self.addCleanup(safe_remove_image, image)
  302. web = self.create_service('web', image=image)
  303. container = web.create_container()
  304. # update the image
  305. c = self.client.create_container(image, ['touch', '/hello.txt'], host_config={})
  306. # In the case of a cluster, there's a chance we pick up the old image when
  307. # calculating the new hash. To circumvent that, untag the old image first
  308. # See also: https://github.com/moby/moby/issues/26852
  309. self.client.remove_image(image, force=True)
  310. self.client.commit(c, repository=repo, tag=tag)
  311. self.client.remove_container(c)
  312. web = self.create_service('web', image=image)
  313. assert ('recreate', [container]) == web.convergence_plan()
  314. @no_cluster('Can not guarantee the build will be run on the same node the service is deployed')
  315. def test_trigger_recreate_with_build(self):
  316. context = tempfile.mkdtemp('test_trigger_recreate_with_build')
  317. self.addCleanup(shutil.rmtree, context)
  318. base_image = "FROM busybox\nLABEL com.docker.compose.test_image=true\n"
  319. dockerfile = os.path.join(context, 'Dockerfile')
  320. with open(dockerfile, mode="w") as dockerfile_fh:
  321. dockerfile_fh.write(base_image)
  322. web = self.create_service('web', build={'context': str(context)})
  323. container = web.create_container()
  324. with open(dockerfile, mode="w") as dockerfile_fh:
  325. dockerfile_fh.write(base_image + 'CMD echo hello world\n')
  326. web.build()
  327. web = self.create_service('web', build={'context': str(context)})
  328. assert ('recreate', [container]) == web.convergence_plan()
  329. def test_image_changed_to_build(self):
  330. context = tempfile.mkdtemp('test_image_changed_to_build')
  331. self.addCleanup(shutil.rmtree, context)
  332. with open(os.path.join(context, 'Dockerfile'), mode="w") as dockerfile:
  333. dockerfile.write("""
  334. FROM busybox
  335. LABEL com.docker.compose.test_image=true
  336. """)
  337. web = self.create_service('web', image='busybox')
  338. container = web.create_container()
  339. web = self.create_service('web', build={'context': str(context)})
  340. plan = web.convergence_plan()
  341. assert ('recreate', [container]) == plan
  342. containers = web.execute_convergence_plan(plan)
  343. assert len(containers) == 1