cli_test.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. from __future__ import absolute_import
  2. import sys
  3. import os
  4. from six import StringIO
  5. from mock import patch
  6. from .testcases import DockerClientTestCase
  7. from compose.cli.main import TopLevelCommand
  8. class CLITestCase(DockerClientTestCase):
  9. def setUp(self):
  10. super(CLITestCase, self).setUp()
  11. self.old_sys_exit = sys.exit
  12. sys.exit = lambda code=0: None
  13. self.command = TopLevelCommand()
  14. self.command.base_dir = 'tests/fixtures/simple-composefile'
  15. def tearDown(self):
  16. sys.exit = self.old_sys_exit
  17. self.project.kill()
  18. self.project.remove_stopped()
  19. for container in self.project.containers(stopped=True, one_off=True):
  20. container.remove(force=True)
  21. @property
  22. def project(self):
  23. # Hack: allow project to be overridden. This needs refactoring so that
  24. # the project object is built exactly once, by the command object, and
  25. # accessed by the test case object.
  26. if hasattr(self, '_project'):
  27. return self._project
  28. return self.command.get_project(self.command.get_config_path())
  29. def test_help(self):
  30. old_base_dir = self.command.base_dir
  31. self.command.base_dir = 'tests/fixtures/no-composefile'
  32. with self.assertRaises(SystemExit) as exc_context:
  33. self.command.dispatch(['help', 'up'], None)
  34. self.assertIn('Usage: up [options] [SERVICE...]', str(exc_context.exception))
  35. # self.project.kill() fails during teardown
  36. # unless there is a composefile.
  37. self.command.base_dir = old_base_dir
  38. # TODO: address the "Inappropriate ioctl for device" warnings in test output
  39. @patch('sys.stdout', new_callable=StringIO)
  40. def test_ps(self, mock_stdout):
  41. self.project.get_service('simple').create_container()
  42. self.command.dispatch(['ps'], None)
  43. self.assertIn('simplecomposefile_simple_1', mock_stdout.getvalue())
  44. @patch('sys.stdout', new_callable=StringIO)
  45. def test_ps_default_composefile(self, mock_stdout):
  46. self.command.base_dir = 'tests/fixtures/multiple-composefiles'
  47. self.command.dispatch(['up', '-d'], None)
  48. self.command.dispatch(['ps'], None)
  49. output = mock_stdout.getvalue()
  50. self.assertIn('multiplecomposefiles_simple_1', output)
  51. self.assertIn('multiplecomposefiles_another_1', output)
  52. self.assertNotIn('multiplecomposefiles_yetanother_1', output)
  53. @patch('sys.stdout', new_callable=StringIO)
  54. def test_ps_alternate_composefile(self, mock_stdout):
  55. config_path = os.path.abspath(
  56. 'tests/fixtures/multiple-composefiles/compose2.yml')
  57. self._project = self.command.get_project(config_path)
  58. self.command.base_dir = 'tests/fixtures/multiple-composefiles'
  59. self.command.dispatch(['-f', 'compose2.yml', 'up', '-d'], None)
  60. self.command.dispatch(['-f', 'compose2.yml', 'ps'], None)
  61. output = mock_stdout.getvalue()
  62. self.assertNotIn('multiplecomposefiles_simple_1', output)
  63. self.assertNotIn('multiplecomposefiles_another_1', output)
  64. self.assertIn('multiplecomposefiles_yetanother_1', output)
  65. @patch('compose.service.log')
  66. def test_pull(self, mock_logging):
  67. self.command.dispatch(['pull'], None)
  68. mock_logging.info.assert_any_call('Pulling simple (busybox:latest)...')
  69. mock_logging.info.assert_any_call('Pulling another (busybox:latest)...')
  70. @patch('sys.stdout', new_callable=StringIO)
  71. def test_build_no_cache(self, mock_stdout):
  72. self.command.base_dir = 'tests/fixtures/simple-dockerfile'
  73. self.command.dispatch(['build', 'simple'], None)
  74. mock_stdout.truncate(0)
  75. cache_indicator = 'Using cache'
  76. self.command.dispatch(['build', 'simple'], None)
  77. output = mock_stdout.getvalue()
  78. self.assertIn(cache_indicator, output)
  79. mock_stdout.truncate(0)
  80. self.command.dispatch(['build', '--no-cache', 'simple'], None)
  81. output = mock_stdout.getvalue()
  82. self.assertNotIn(cache_indicator, output)
  83. def test_up(self):
  84. self.command.dispatch(['up', '-d'], None)
  85. service = self.project.get_service('simple')
  86. another = self.project.get_service('another')
  87. self.assertEqual(len(service.containers()), 1)
  88. self.assertEqual(len(another.containers()), 1)
  89. # Ensure containers don't have stdin and stdout connected in -d mode
  90. config = service.containers()[0].inspect()['Config']
  91. self.assertFalse(config['AttachStderr'])
  92. self.assertFalse(config['AttachStdout'])
  93. self.assertFalse(config['AttachStdin'])
  94. def test_up_with_links(self):
  95. self.command.base_dir = 'tests/fixtures/links-composefile'
  96. self.command.dispatch(['up', '-d', 'web'], None)
  97. web = self.project.get_service('web')
  98. db = self.project.get_service('db')
  99. console = self.project.get_service('console')
  100. self.assertEqual(len(web.containers()), 1)
  101. self.assertEqual(len(db.containers()), 1)
  102. self.assertEqual(len(console.containers()), 0)
  103. def test_up_with_no_deps(self):
  104. self.command.base_dir = 'tests/fixtures/links-composefile'
  105. self.command.dispatch(['up', '-d', '--no-deps', 'web'], None)
  106. web = self.project.get_service('web')
  107. db = self.project.get_service('db')
  108. console = self.project.get_service('console')
  109. self.assertEqual(len(web.containers()), 1)
  110. self.assertEqual(len(db.containers()), 0)
  111. self.assertEqual(len(console.containers()), 0)
  112. def test_up_with_recreate(self):
  113. self.command.dispatch(['up', '-d'], None)
  114. service = self.project.get_service('simple')
  115. self.assertEqual(len(service.containers()), 1)
  116. old_ids = [c.id for c in service.containers()]
  117. self.command.dispatch(['up', '-d'], None)
  118. self.assertEqual(len(service.containers()), 1)
  119. new_ids = [c.id for c in service.containers()]
  120. self.assertNotEqual(old_ids, new_ids)
  121. def test_up_with_keep_old(self):
  122. self.command.dispatch(['up', '-d'], None)
  123. service = self.project.get_service('simple')
  124. self.assertEqual(len(service.containers()), 1)
  125. old_ids = [c.id for c in service.containers()]
  126. self.command.dispatch(['up', '-d', '--no-recreate'], None)
  127. self.assertEqual(len(service.containers()), 1)
  128. new_ids = [c.id for c in service.containers()]
  129. self.assertEqual(old_ids, new_ids)
  130. @patch('dockerpty.start')
  131. def test_run_service_without_links(self, mock_stdout):
  132. self.command.base_dir = 'tests/fixtures/links-composefile'
  133. self.command.dispatch(['run', 'console', '/bin/true'], None)
  134. self.assertEqual(len(self.project.containers()), 0)
  135. # Ensure stdin/out was open
  136. container = self.project.containers(stopped=True, one_off=True)[0]
  137. config = container.inspect()['Config']
  138. self.assertTrue(config['AttachStderr'])
  139. self.assertTrue(config['AttachStdout'])
  140. self.assertTrue(config['AttachStdin'])
  141. @patch('dockerpty.start')
  142. def test_run_service_with_links(self, __):
  143. self.command.base_dir = 'tests/fixtures/links-composefile'
  144. self.command.dispatch(['run', 'web', '/bin/true'], None)
  145. db = self.project.get_service('db')
  146. console = self.project.get_service('console')
  147. self.assertEqual(len(db.containers()), 1)
  148. self.assertEqual(len(console.containers()), 0)
  149. @patch('dockerpty.start')
  150. def test_run_with_no_deps(self, __):
  151. self.command.base_dir = 'tests/fixtures/links-composefile'
  152. self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None)
  153. db = self.project.get_service('db')
  154. self.assertEqual(len(db.containers()), 0)
  155. @patch('dockerpty.start')
  156. def test_run_does_not_recreate_linked_containers(self, __):
  157. self.command.base_dir = 'tests/fixtures/links-composefile'
  158. self.command.dispatch(['up', '-d', 'db'], None)
  159. db = self.project.get_service('db')
  160. self.assertEqual(len(db.containers()), 1)
  161. old_ids = [c.id for c in db.containers()]
  162. self.command.dispatch(['run', 'web', '/bin/true'], None)
  163. self.assertEqual(len(db.containers()), 1)
  164. new_ids = [c.id for c in db.containers()]
  165. self.assertEqual(old_ids, new_ids)
  166. @patch('dockerpty.start')
  167. def test_run_without_command(self, __):
  168. self.command.base_dir = 'tests/fixtures/commands-composefile'
  169. self.check_build('tests/fixtures/simple-dockerfile', tag='composetest_test')
  170. for c in self.project.containers(stopped=True, one_off=True):
  171. c.remove()
  172. self.command.dispatch(['run', 'implicit'], None)
  173. service = self.project.get_service('implicit')
  174. containers = service.containers(stopped=True, one_off=True)
  175. self.assertEqual(
  176. [c.human_readable_command for c in containers],
  177. [u'/bin/sh -c echo "success"'],
  178. )
  179. self.command.dispatch(['run', 'explicit'], None)
  180. service = self.project.get_service('explicit')
  181. containers = service.containers(stopped=True, one_off=True)
  182. self.assertEqual(
  183. [c.human_readable_command for c in containers],
  184. [u'/bin/true'],
  185. )
  186. @patch('dockerpty.start')
  187. def test_run_service_with_entrypoint_overridden(self, _):
  188. self.command.base_dir = 'tests/fixtures/dockerfile_with_entrypoint'
  189. name = 'service'
  190. self.command.dispatch(
  191. ['run', '--entrypoint', '/bin/echo', name, 'helloworld'],
  192. None
  193. )
  194. service = self.project.get_service(name)
  195. container = service.containers(stopped=True, one_off=True)[0]
  196. self.assertEqual(
  197. container.human_readable_command,
  198. u'/bin/echo helloworld'
  199. )
  200. @patch('dockerpty.start')
  201. def test_run_service_with_user_overridden(self, _):
  202. self.command.base_dir = 'tests/fixtures/user-composefile'
  203. name = 'service'
  204. user = 'sshd'
  205. args = ['run', '--user={}'.format(user), name]
  206. self.command.dispatch(args, None)
  207. service = self.project.get_service(name)
  208. container = service.containers(stopped=True, one_off=True)[0]
  209. self.assertEqual(user, container.get('Config.User'))
  210. @patch('dockerpty.start')
  211. def test_run_service_with_user_overridden_short_form(self, _):
  212. self.command.base_dir = 'tests/fixtures/user-composefile'
  213. name = 'service'
  214. user = 'sshd'
  215. args = ['run', '-u', user, name]
  216. self.command.dispatch(args, None)
  217. service = self.project.get_service(name)
  218. container = service.containers(stopped=True, one_off=True)[0]
  219. self.assertEqual(user, container.get('Config.User'))
  220. @patch('dockerpty.start')
  221. def test_run_service_with_environement_overridden(self, _):
  222. name = 'service'
  223. self.command.base_dir = 'tests/fixtures/environment-composefile'
  224. self.command.dispatch(
  225. ['run', '-e', 'foo=notbar', '-e', 'allo=moto=bobo',
  226. '-e', 'alpha=beta', name],
  227. None
  228. )
  229. service = self.project.get_service(name)
  230. container = service.containers(stopped=True, one_off=True)[0]
  231. # env overriden
  232. self.assertEqual('notbar', container.environment['foo'])
  233. # keep environement from yaml
  234. self.assertEqual('world', container.environment['hello'])
  235. # added option from command line
  236. self.assertEqual('beta', container.environment['alpha'])
  237. # make sure a value with a = don't crash out
  238. self.assertEqual('moto=bobo', container.environment['allo'])
  239. @patch('dockerpty.start')
  240. def test_run_service_without_map_ports(self, __):
  241. # create one off container
  242. self.command.base_dir = 'tests/fixtures/ports-composefile'
  243. self.command.dispatch(['run', '-d', 'simple'], None)
  244. container = self.project.get_service('simple').containers(one_off=True)[0]
  245. # get port information
  246. port_random = container.get_local_port(3000)
  247. port_assigned = container.get_local_port(3001)
  248. # close all one off containers we just created
  249. container.stop()
  250. # check the ports
  251. self.assertEqual(port_random, None)
  252. self.assertEqual(port_assigned, None)
  253. @patch('dockerpty.start')
  254. def test_run_service_with_map_ports(self, __):
  255. # create one off container
  256. self.command.base_dir = 'tests/fixtures/ports-composefile'
  257. self.command.dispatch(['run', '-d', '--service-ports', 'simple'], None)
  258. container = self.project.get_service('simple').containers(one_off=True)[0]
  259. # get port information
  260. port_random = container.get_local_port(3000)
  261. port_assigned = container.get_local_port(3001)
  262. # close all one off containers we just created
  263. container.stop()
  264. # check the ports
  265. self.assertNotEqual(port_random, None)
  266. self.assertIn("0.0.0.0", port_random)
  267. self.assertEqual(port_assigned, "0.0.0.0:49152")
  268. def test_rm(self):
  269. service = self.project.get_service('simple')
  270. service.create_container()
  271. service.kill()
  272. self.assertEqual(len(service.containers(stopped=True)), 1)
  273. self.command.dispatch(['rm', '--force'], None)
  274. self.assertEqual(len(service.containers(stopped=True)), 0)
  275. service = self.project.get_service('simple')
  276. service.create_container()
  277. service.kill()
  278. self.assertEqual(len(service.containers(stopped=True)), 1)
  279. self.command.dispatch(['rm', '-f'], None)
  280. self.assertEqual(len(service.containers(stopped=True)), 0)
  281. def test_stop(self):
  282. self.command.dispatch(['up', '-d'], None)
  283. service = self.project.get_service('simple')
  284. self.assertEqual(len(service.containers()), 1)
  285. self.assertTrue(service.containers()[0].is_running)
  286. self.command.dispatch(['stop', '-t', '1'], None)
  287. self.assertEqual(len(service.containers(stopped=True)), 1)
  288. self.assertFalse(service.containers(stopped=True)[0].is_running)
  289. def test_kill(self):
  290. self.command.dispatch(['up', '-d'], None)
  291. service = self.project.get_service('simple')
  292. self.assertEqual(len(service.containers()), 1)
  293. self.assertTrue(service.containers()[0].is_running)
  294. self.command.dispatch(['kill'], None)
  295. self.assertEqual(len(service.containers(stopped=True)), 1)
  296. self.assertFalse(service.containers(stopped=True)[0].is_running)
  297. def test_kill_signal_sigint(self):
  298. self.command.dispatch(['up', '-d'], None)
  299. service = self.project.get_service('simple')
  300. self.assertEqual(len(service.containers()), 1)
  301. self.assertTrue(service.containers()[0].is_running)
  302. self.command.dispatch(['kill', '-s', 'SIGINT'], None)
  303. self.assertEqual(len(service.containers()), 1)
  304. # The container is still running. It has been only interrupted
  305. self.assertTrue(service.containers()[0].is_running)
  306. def test_kill_interrupted_service(self):
  307. self.command.dispatch(['up', '-d'], None)
  308. service = self.project.get_service('simple')
  309. self.command.dispatch(['kill', '-s', 'SIGINT'], None)
  310. self.assertTrue(service.containers()[0].is_running)
  311. self.command.dispatch(['kill', '-s', 'SIGKILL'], None)
  312. self.assertEqual(len(service.containers(stopped=True)), 1)
  313. self.assertFalse(service.containers(stopped=True)[0].is_running)
  314. def test_restart(self):
  315. service = self.project.get_service('simple')
  316. container = service.create_container()
  317. service.start_container(container)
  318. started_at = container.dictionary['State']['StartedAt']
  319. self.command.dispatch(['restart', '-t', '1'], None)
  320. container.inspect()
  321. self.assertNotEqual(
  322. container.dictionary['State']['FinishedAt'],
  323. '0001-01-01T00:00:00Z',
  324. )
  325. self.assertNotEqual(
  326. container.dictionary['State']['StartedAt'],
  327. started_at,
  328. )
  329. def test_scale(self):
  330. project = self.project
  331. self.command.scale(project, {'SERVICE=NUM': ['simple=1']})
  332. self.assertEqual(len(project.get_service('simple').containers()), 1)
  333. self.command.scale(project, {'SERVICE=NUM': ['simple=3', 'another=2']})
  334. self.assertEqual(len(project.get_service('simple').containers()), 3)
  335. self.assertEqual(len(project.get_service('another').containers()), 2)
  336. self.command.scale(project, {'SERVICE=NUM': ['simple=1', 'another=1']})
  337. self.assertEqual(len(project.get_service('simple').containers()), 1)
  338. self.assertEqual(len(project.get_service('another').containers()), 1)
  339. self.command.scale(project, {'SERVICE=NUM': ['simple=1', 'another=1']})
  340. self.assertEqual(len(project.get_service('simple').containers()), 1)
  341. self.assertEqual(len(project.get_service('another').containers()), 1)
  342. self.command.scale(project, {'SERVICE=NUM': ['simple=0', 'another=0']})
  343. self.assertEqual(len(project.get_service('simple').containers()), 0)
  344. self.assertEqual(len(project.get_service('another').containers()), 0)
  345. def test_port(self):
  346. self.command.base_dir = 'tests/fixtures/ports-composefile'
  347. self.command.dispatch(['up', '-d'], None)
  348. container = self.project.get_service('simple').get_container()
  349. @patch('sys.stdout', new_callable=StringIO)
  350. def get_port(number, mock_stdout):
  351. self.command.dispatch(['port', 'simple', str(number)], None)
  352. return mock_stdout.getvalue().rstrip()
  353. self.assertEqual(get_port(3000), container.get_local_port(3000))
  354. self.assertEqual(get_port(3001), "0.0.0.0:49152")
  355. self.assertEqual(get_port(3002), "")
  356. def test_env_file_relative_to_compose_file(self):
  357. config_path = os.path.abspath('tests/fixtures/env-file/docker-compose.yml')
  358. self.command.dispatch(['-f', config_path, 'up', '-d'], None)
  359. self._project = self.command.get_project(config_path)
  360. containers = self.project.containers(stopped=True)
  361. self.assertEqual(len(containers), 1)
  362. self.assertIn("FOO=1", containers[0].get('Config.Env'))
  363. def test_up_with_extends(self):
  364. self.command.base_dir = 'tests/fixtures/extends'
  365. self.command.dispatch(['up', '-d'], None)
  366. self.assertEqual(
  367. set([s.name for s in self.project.services]),
  368. set(['mydb', 'myweb']),
  369. )
  370. # Sort by name so we get [db, web]
  371. containers = sorted(
  372. self.project.containers(stopped=True),
  373. key=lambda c: c.name,
  374. )
  375. self.assertEqual(len(containers), 2)
  376. web = containers[1]
  377. self.assertEqual(set(web.links()), set(['db', 'mydb_1', 'extends_mydb_1']))
  378. expected_env = set([
  379. "FOO=1",
  380. "BAR=2",
  381. "BAZ=2",
  382. ])
  383. self.assertTrue(expected_env <= set(web.get('Config.Env')))