service_test.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. from __future__ import unicode_literals
  2. from __future__ import absolute_import
  3. import os
  4. from os import path
  5. from docker.errors import APIError
  6. from mock import patch
  7. import tempfile
  8. import shutil
  9. from six import StringIO, text_type
  10. from compose import __version__
  11. from compose.const import (
  12. LABEL_CONTAINER_NUMBER,
  13. LABEL_ONE_OFF,
  14. LABEL_PROJECT,
  15. LABEL_SERVICE,
  16. LABEL_VERSION,
  17. )
  18. from compose.service import (
  19. ConfigError,
  20. ConvergencePlan,
  21. Service,
  22. build_extra_hosts,
  23. )
  24. from compose.container import Container
  25. from .testcases import DockerClientTestCase
  26. def create_and_start_container(service, **override_options):
  27. container = service.create_container(**override_options)
  28. return service.start_container(container)
  29. class ServiceTest(DockerClientTestCase):
  30. def test_containers(self):
  31. foo = self.create_service('foo')
  32. bar = self.create_service('bar')
  33. create_and_start_container(foo)
  34. self.assertEqual(len(foo.containers()), 1)
  35. self.assertEqual(foo.containers()[0].name, 'composetest_foo_1')
  36. self.assertEqual(len(bar.containers()), 0)
  37. create_and_start_container(bar)
  38. create_and_start_container(bar)
  39. self.assertEqual(len(foo.containers()), 1)
  40. self.assertEqual(len(bar.containers()), 2)
  41. names = [c.name for c in bar.containers()]
  42. self.assertIn('composetest_bar_1', names)
  43. self.assertIn('composetest_bar_2', names)
  44. def test_containers_one_off(self):
  45. db = self.create_service('db')
  46. container = db.create_container(one_off=True)
  47. self.assertEqual(db.containers(stopped=True), [])
  48. self.assertEqual(db.containers(one_off=True, stopped=True), [container])
  49. def test_project_is_added_to_container_name(self):
  50. service = self.create_service('web')
  51. create_and_start_container(service)
  52. self.assertEqual(service.containers()[0].name, 'composetest_web_1')
  53. def test_start_stop(self):
  54. service = self.create_service('scalingtest')
  55. self.assertEqual(len(service.containers(stopped=True)), 0)
  56. service.create_container()
  57. self.assertEqual(len(service.containers()), 0)
  58. self.assertEqual(len(service.containers(stopped=True)), 1)
  59. service.start()
  60. self.assertEqual(len(service.containers()), 1)
  61. self.assertEqual(len(service.containers(stopped=True)), 1)
  62. service.stop(timeout=1)
  63. self.assertEqual(len(service.containers()), 0)
  64. self.assertEqual(len(service.containers(stopped=True)), 1)
  65. service.stop(timeout=1)
  66. self.assertEqual(len(service.containers()), 0)
  67. self.assertEqual(len(service.containers(stopped=True)), 1)
  68. def test_kill_remove(self):
  69. service = self.create_service('scalingtest')
  70. create_and_start_container(service)
  71. self.assertEqual(len(service.containers()), 1)
  72. service.remove_stopped()
  73. self.assertEqual(len(service.containers()), 1)
  74. service.kill()
  75. self.assertEqual(len(service.containers()), 0)
  76. self.assertEqual(len(service.containers(stopped=True)), 1)
  77. service.remove_stopped()
  78. self.assertEqual(len(service.containers(stopped=True)), 0)
  79. def test_create_container_with_one_off(self):
  80. db = self.create_service('db')
  81. container = db.create_container(one_off=True)
  82. self.assertEqual(container.name, 'composetest_db_run_1')
  83. def test_create_container_with_one_off_when_existing_container_is_running(self):
  84. db = self.create_service('db')
  85. db.start()
  86. container = db.create_container(one_off=True)
  87. self.assertEqual(container.name, 'composetest_db_run_1')
  88. def test_create_container_with_unspecified_volume(self):
  89. service = self.create_service('db', volumes=['/var/db'])
  90. container = service.create_container()
  91. service.start_container(container)
  92. self.assertIn('/var/db', container.get('Volumes'))
  93. def test_create_container_with_cpu_shares(self):
  94. service = self.create_service('db', cpu_shares=73)
  95. container = service.create_container()
  96. service.start_container(container)
  97. self.assertEqual(container.get('HostConfig.CpuShares'), 73)
  98. def test_build_extra_hosts(self):
  99. # string
  100. self.assertRaises(ConfigError, lambda: build_extra_hosts("www.example.com: 192.168.0.17"))
  101. # list of strings
  102. self.assertEqual(build_extra_hosts(
  103. ["www.example.com:192.168.0.17"]),
  104. {'www.example.com': '192.168.0.17'})
  105. self.assertEqual(build_extra_hosts(
  106. ["www.example.com: 192.168.0.17"]),
  107. {'www.example.com': '192.168.0.17'})
  108. self.assertEqual(build_extra_hosts(
  109. ["www.example.com: 192.168.0.17",
  110. "static.example.com:192.168.0.19",
  111. "api.example.com: 192.168.0.18"]),
  112. {'www.example.com': '192.168.0.17',
  113. 'static.example.com': '192.168.0.19',
  114. 'api.example.com': '192.168.0.18'})
  115. # list of dictionaries
  116. self.assertRaises(ConfigError, lambda: build_extra_hosts(
  117. [{'www.example.com': '192.168.0.17'},
  118. {'api.example.com': '192.168.0.18'}]))
  119. # dictionaries
  120. self.assertEqual(build_extra_hosts(
  121. {'www.example.com': '192.168.0.17',
  122. 'api.example.com': '192.168.0.18'}),
  123. {'www.example.com': '192.168.0.17',
  124. 'api.example.com': '192.168.0.18'})
  125. def test_create_container_with_extra_hosts_list(self):
  126. extra_hosts = ['somehost:162.242.195.82', 'otherhost:50.31.209.229']
  127. service = self.create_service('db', extra_hosts=extra_hosts)
  128. container = service.create_container()
  129. service.start_container(container)
  130. self.assertEqual(set(container.get('HostConfig.ExtraHosts')), set(extra_hosts))
  131. def test_create_container_with_extra_hosts_string(self):
  132. extra_hosts = 'somehost:162.242.195.82'
  133. service = self.create_service('db', extra_hosts=extra_hosts)
  134. self.assertRaises(ConfigError, lambda: service.create_container())
  135. def test_create_container_with_extra_hosts_list_of_dicts(self):
  136. extra_hosts = [{'somehost': '162.242.195.82'}, {'otherhost': '50.31.209.229'}]
  137. service = self.create_service('db', extra_hosts=extra_hosts)
  138. self.assertRaises(ConfigError, lambda: service.create_container())
  139. def test_create_container_with_extra_hosts_dicts(self):
  140. extra_hosts = {'somehost': '162.242.195.82', 'otherhost': '50.31.209.229'}
  141. extra_hosts_list = ['somehost:162.242.195.82', 'otherhost:50.31.209.229']
  142. service = self.create_service('db', extra_hosts=extra_hosts)
  143. container = service.create_container()
  144. service.start_container(container)
  145. self.assertEqual(set(container.get('HostConfig.ExtraHosts')), set(extra_hosts_list))
  146. def test_create_container_with_cpu_set(self):
  147. service = self.create_service('db', cpuset='0')
  148. container = service.create_container()
  149. service.start_container(container)
  150. self.assertEqual(container.get('HostConfig.CpusetCpus'), '0')
  151. def test_create_container_with_read_only_root_fs(self):
  152. read_only = True
  153. service = self.create_service('db', read_only=read_only)
  154. container = service.create_container()
  155. service.start_container(container)
  156. self.assertEqual(container.get('HostConfig.ReadonlyRootfs'), read_only, container.get('HostConfig'))
  157. def test_create_container_with_security_opt(self):
  158. security_opt = ['label:disable']
  159. service = self.create_service('db', security_opt=security_opt)
  160. container = service.create_container()
  161. service.start_container(container)
  162. self.assertEqual(set(container.get('HostConfig.SecurityOpt')), set(security_opt))
  163. def test_create_container_with_mac_address(self):
  164. service = self.create_service('db', mac_address='02:42:ac:11:65:43')
  165. container = service.create_container()
  166. service.start_container(container)
  167. self.assertEqual(container.inspect()['Config']['MacAddress'], '02:42:ac:11:65:43')
  168. def test_create_container_with_specified_volume(self):
  169. host_path = '/tmp/host-path'
  170. container_path = '/container-path'
  171. service = self.create_service('db', volumes=['%s:%s' % (host_path, container_path)])
  172. container = service.create_container()
  173. service.start_container(container)
  174. volumes = container.inspect()['Volumes']
  175. self.assertIn(container_path, volumes)
  176. # Match the last component ("host-path"), because boot2docker symlinks /tmp
  177. actual_host_path = volumes[container_path]
  178. self.assertTrue(path.basename(actual_host_path) == path.basename(host_path),
  179. msg=("Last component differs: %s, %s" % (actual_host_path, host_path)))
  180. @patch.dict(os.environ)
  181. def test_create_container_with_home_and_env_var_in_volume_path(self):
  182. os.environ['VOLUME_NAME'] = 'my-volume'
  183. os.environ['HOME'] = '/tmp/home-dir'
  184. expected_host_path = os.path.join(os.environ['HOME'], os.environ['VOLUME_NAME'])
  185. host_path = '~/${VOLUME_NAME}'
  186. container_path = '/container-path'
  187. service = self.create_service('db', volumes=['%s:%s' % (host_path, container_path)])
  188. container = service.create_container()
  189. service.start_container(container)
  190. actual_host_path = container.get('Volumes')[container_path]
  191. components = actual_host_path.split('/')
  192. self.assertTrue(components[-2:] == ['home-dir', 'my-volume'],
  193. msg="Last two components differ: %s, %s" % (actual_host_path, expected_host_path))
  194. def test_create_container_with_volumes_from(self):
  195. volume_service = self.create_service('data')
  196. volume_container_1 = volume_service.create_container()
  197. volume_container_2 = Container.create(
  198. self.client,
  199. image='busybox:latest',
  200. command=["top"],
  201. labels={LABEL_PROJECT: 'composetest'},
  202. )
  203. host_service = self.create_service('host', volumes_from=[volume_service, volume_container_2])
  204. host_container = host_service.create_container()
  205. host_service.start_container(host_container)
  206. self.assertIn(volume_container_1.id,
  207. host_container.get('HostConfig.VolumesFrom'))
  208. self.assertIn(volume_container_2.id,
  209. host_container.get('HostConfig.VolumesFrom'))
  210. def test_execute_convergence_plan_recreate(self):
  211. service = self.create_service(
  212. 'db',
  213. environment={'FOO': '1'},
  214. volumes=['/etc'],
  215. entrypoint=['top'],
  216. command=['-d', '1']
  217. )
  218. old_container = service.create_container()
  219. self.assertEqual(old_container.get('Config.Entrypoint'), ['top'])
  220. self.assertEqual(old_container.get('Config.Cmd'), ['-d', '1'])
  221. self.assertIn('FOO=1', old_container.get('Config.Env'))
  222. self.assertEqual(old_container.name, 'composetest_db_1')
  223. service.start_container(old_container)
  224. old_container.inspect() # reload volume data
  225. volume_path = old_container.get('Volumes')['/etc']
  226. num_containers_before = len(self.client.containers(all=True))
  227. service.options['environment']['FOO'] = '2'
  228. new_container, = service.execute_convergence_plan(
  229. ConvergencePlan('recreate', [old_container]))
  230. self.assertEqual(new_container.get('Config.Entrypoint'), ['top'])
  231. self.assertEqual(new_container.get('Config.Cmd'), ['-d', '1'])
  232. self.assertIn('FOO=2', new_container.get('Config.Env'))
  233. self.assertEqual(new_container.name, 'composetest_db_1')
  234. self.assertEqual(new_container.get('Volumes')['/etc'], volume_path)
  235. self.assertIn(
  236. 'affinity:container==%s' % old_container.id,
  237. new_container.get('Config.Env'))
  238. self.assertEqual(len(self.client.containers(all=True)), num_containers_before)
  239. self.assertNotEqual(old_container.id, new_container.id)
  240. self.assertRaises(APIError,
  241. self.client.inspect_container,
  242. old_container.id)
  243. def test_execute_convergence_plan_when_containers_are_stopped(self):
  244. service = self.create_service(
  245. 'db',
  246. environment={'FOO': '1'},
  247. volumes=['/var/db'],
  248. entrypoint=['top'],
  249. command=['-d', '1']
  250. )
  251. service.create_container()
  252. containers = service.containers(stopped=True)
  253. self.assertEqual(len(containers), 1)
  254. container, = containers
  255. self.assertFalse(container.is_running)
  256. service.execute_convergence_plan(ConvergencePlan('start', [container]))
  257. containers = service.containers()
  258. self.assertEqual(len(containers), 1)
  259. container.inspect()
  260. self.assertEqual(container, containers[0])
  261. self.assertTrue(container.is_running)
  262. def test_execute_convergence_plan_with_image_declared_volume(self):
  263. service = Service(
  264. project='composetest',
  265. name='db',
  266. client=self.client,
  267. build='tests/fixtures/dockerfile-with-volume',
  268. )
  269. old_container = create_and_start_container(service)
  270. self.assertEqual(old_container.get('Volumes').keys(), ['/data'])
  271. volume_path = old_container.get('Volumes')['/data']
  272. new_container, = service.execute_convergence_plan(
  273. ConvergencePlan('recreate', [old_container]))
  274. self.assertEqual(new_container.get('Volumes').keys(), ['/data'])
  275. self.assertEqual(new_container.get('Volumes')['/data'], volume_path)
  276. def test_start_container_passes_through_options(self):
  277. db = self.create_service('db')
  278. create_and_start_container(db, environment={'FOO': 'BAR'})
  279. self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
  280. def test_start_container_inherits_options_from_constructor(self):
  281. db = self.create_service('db', environment={'FOO': 'BAR'})
  282. create_and_start_container(db)
  283. self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
  284. def test_start_container_creates_links(self):
  285. db = self.create_service('db')
  286. web = self.create_service('web', links=[(db, None)])
  287. create_and_start_container(db)
  288. create_and_start_container(db)
  289. create_and_start_container(web)
  290. self.assertEqual(
  291. set(web.containers()[0].links()),
  292. set([
  293. 'composetest_db_1', 'db_1',
  294. 'composetest_db_2', 'db_2',
  295. 'db'])
  296. )
  297. def test_start_container_creates_links_with_names(self):
  298. db = self.create_service('db')
  299. web = self.create_service('web', links=[(db, 'custom_link_name')])
  300. create_and_start_container(db)
  301. create_and_start_container(db)
  302. create_and_start_container(web)
  303. self.assertEqual(
  304. set(web.containers()[0].links()),
  305. set([
  306. 'composetest_db_1', 'db_1',
  307. 'composetest_db_2', 'db_2',
  308. 'custom_link_name'])
  309. )
  310. def test_start_container_with_external_links(self):
  311. db = self.create_service('db')
  312. web = self.create_service('web', external_links=['composetest_db_1',
  313. 'composetest_db_2',
  314. 'composetest_db_3:db_3'])
  315. for _ in range(3):
  316. create_and_start_container(db)
  317. create_and_start_container(web)
  318. self.assertEqual(
  319. set(web.containers()[0].links()),
  320. set([
  321. 'composetest_db_1',
  322. 'composetest_db_2',
  323. 'db_3']),
  324. )
  325. def test_start_normal_container_does_not_create_links_to_its_own_service(self):
  326. db = self.create_service('db')
  327. create_and_start_container(db)
  328. create_and_start_container(db)
  329. c = create_and_start_container(db)
  330. self.assertEqual(set(c.links()), set([]))
  331. def test_start_one_off_container_creates_links_to_its_own_service(self):
  332. db = self.create_service('db')
  333. create_and_start_container(db)
  334. create_and_start_container(db)
  335. c = create_and_start_container(db, one_off=True)
  336. self.assertEqual(
  337. set(c.links()),
  338. set([
  339. 'composetest_db_1', 'db_1',
  340. 'composetest_db_2', 'db_2',
  341. 'db'])
  342. )
  343. def test_start_container_builds_images(self):
  344. service = Service(
  345. name='test',
  346. client=self.client,
  347. build='tests/fixtures/simple-dockerfile',
  348. project='composetest',
  349. )
  350. container = create_and_start_container(service)
  351. container.wait()
  352. self.assertIn('success', container.logs())
  353. self.assertEqual(len(self.client.images(name='composetest_test')), 1)
  354. def test_start_container_uses_tagged_image_if_it_exists(self):
  355. self.check_build('tests/fixtures/simple-dockerfile', tag='composetest_test')
  356. service = Service(
  357. name='test',
  358. client=self.client,
  359. build='this/does/not/exist/and/will/throw/error',
  360. project='composetest',
  361. )
  362. container = create_and_start_container(service)
  363. container.wait()
  364. self.assertIn('success', container.logs())
  365. def test_start_container_creates_ports(self):
  366. service = self.create_service('web', ports=[8000])
  367. container = create_and_start_container(service).inspect()
  368. self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/tcp'])
  369. self.assertNotEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000')
  370. def test_build(self):
  371. base_dir = tempfile.mkdtemp()
  372. self.addCleanup(shutil.rmtree, base_dir)
  373. with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
  374. f.write("FROM busybox\n")
  375. self.create_service('web', build=base_dir).build()
  376. self.assertEqual(len(self.client.images(name='composetest_web')), 1)
  377. def test_build_non_ascii_filename(self):
  378. base_dir = tempfile.mkdtemp()
  379. self.addCleanup(shutil.rmtree, base_dir)
  380. with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
  381. f.write("FROM busybox\n")
  382. with open(os.path.join(base_dir, b'foo\xE2bar'), 'w') as f:
  383. f.write("hello world\n")
  384. self.create_service('web', build=text_type(base_dir)).build()
  385. self.assertEqual(len(self.client.images(name='composetest_web')), 1)
  386. def test_start_container_stays_unpriviliged(self):
  387. service = self.create_service('web')
  388. container = create_and_start_container(service).inspect()
  389. self.assertEqual(container['HostConfig']['Privileged'], False)
  390. def test_start_container_becomes_priviliged(self):
  391. service = self.create_service('web', privileged=True)
  392. container = create_and_start_container(service).inspect()
  393. self.assertEqual(container['HostConfig']['Privileged'], True)
  394. def test_expose_does_not_publish_ports(self):
  395. service = self.create_service('web', expose=[8000])
  396. container = create_and_start_container(service).inspect()
  397. self.assertEqual(container['NetworkSettings']['Ports'], {'8000/tcp': None})
  398. def test_start_container_creates_port_with_explicit_protocol(self):
  399. service = self.create_service('web', ports=['8000/udp'])
  400. container = create_and_start_container(service).inspect()
  401. self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/udp'])
  402. def test_start_container_creates_fixed_external_ports(self):
  403. service = self.create_service('web', ports=['8000:8000'])
  404. container = create_and_start_container(service).inspect()
  405. self.assertIn('8000/tcp', container['NetworkSettings']['Ports'])
  406. self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000')
  407. def test_start_container_creates_fixed_external_ports_when_it_is_different_to_internal_port(self):
  408. service = self.create_service('web', ports=['8001:8000'])
  409. container = create_and_start_container(service).inspect()
  410. self.assertIn('8000/tcp', container['NetworkSettings']['Ports'])
  411. self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8001')
  412. def test_port_with_explicit_interface(self):
  413. service = self.create_service('web', ports=[
  414. '127.0.0.1:8001:8000',
  415. '0.0.0.0:9001:9000/udp',
  416. ])
  417. container = create_and_start_container(service).inspect()
  418. self.assertEqual(container['NetworkSettings']['Ports'], {
  419. '8000/tcp': [
  420. {
  421. 'HostIp': '127.0.0.1',
  422. 'HostPort': '8001',
  423. },
  424. ],
  425. '9000/udp': [
  426. {
  427. 'HostIp': '0.0.0.0',
  428. 'HostPort': '9001',
  429. },
  430. ],
  431. })
  432. def test_create_with_image_id(self):
  433. # Image id for the current busybox:latest
  434. service = self.create_service('foo', image='8c2e06607696')
  435. service.create_container()
  436. def test_scale(self):
  437. service = self.create_service('web')
  438. service.scale(1)
  439. self.assertEqual(len(service.containers()), 1)
  440. # Ensure containers don't have stdout or stdin connected
  441. container = service.containers()[0]
  442. config = container.inspect()['Config']
  443. self.assertFalse(config['AttachStderr'])
  444. self.assertFalse(config['AttachStdout'])
  445. self.assertFalse(config['AttachStdin'])
  446. service.scale(3)
  447. self.assertEqual(len(service.containers()), 3)
  448. service.scale(1)
  449. self.assertEqual(len(service.containers()), 1)
  450. service.scale(0)
  451. self.assertEqual(len(service.containers()), 0)
  452. @patch('sys.stdout', new_callable=StringIO)
  453. def test_scale_with_stopped_containers(self, mock_stdout):
  454. """
  455. Given there are some stopped containers and scale is called with a
  456. desired number that is the same as the number of stopped containers,
  457. test that those containers are restarted and not removed/recreated.
  458. """
  459. service = self.create_service('web')
  460. next_number = service._next_container_number()
  461. valid_numbers = [next_number, next_number + 1]
  462. service.create_container(number=next_number, quiet=True)
  463. service.create_container(number=next_number + 1, quiet=True)
  464. for container in service.containers():
  465. self.assertFalse(container.is_running)
  466. service.scale(2)
  467. self.assertEqual(len(service.containers()), 2)
  468. for container in service.containers():
  469. self.assertTrue(container.is_running)
  470. self.assertTrue(container.number in valid_numbers)
  471. captured_output = mock_stdout.getvalue()
  472. self.assertNotIn('Creating', captured_output)
  473. self.assertIn('Starting', captured_output)
  474. @patch('sys.stdout', new_callable=StringIO)
  475. def test_scale_with_stopped_containers_and_needing_creation(self, mock_stdout):
  476. """
  477. Given there are some stopped containers and scale is called with a
  478. desired number that is greater than the number of stopped containers,
  479. test that those containers are restarted and required number are created.
  480. """
  481. service = self.create_service('web')
  482. next_number = service._next_container_number()
  483. service.create_container(number=next_number, quiet=True)
  484. for container in service.containers():
  485. self.assertFalse(container.is_running)
  486. service.scale(2)
  487. self.assertEqual(len(service.containers()), 2)
  488. for container in service.containers():
  489. self.assertTrue(container.is_running)
  490. captured_output = mock_stdout.getvalue()
  491. self.assertIn('Creating', captured_output)
  492. self.assertIn('Starting', captured_output)
  493. @patch('sys.stdout', new_callable=StringIO)
  494. def test_scale_with_api_returns_errors(self, mock_stdout):
  495. """
  496. Test that when scaling if the API returns an error, that error is handled
  497. and the remaining threads continue.
  498. """
  499. service = self.create_service('web')
  500. next_number = service._next_container_number()
  501. service.create_container(number=next_number, quiet=True)
  502. with patch(
  503. 'compose.container.Container.create',
  504. side_effect=APIError(message="testing", response={}, explanation="Boom")):
  505. service.scale(3)
  506. self.assertEqual(len(service.containers()), 1)
  507. self.assertTrue(service.containers()[0].is_running)
  508. self.assertIn("ERROR: for 2 Boom", mock_stdout.getvalue())
  509. @patch('compose.service.log')
  510. def test_scale_with_desired_number_already_achieved(self, mock_log):
  511. """
  512. Test that calling scale with a desired number that is equal to the
  513. number of containers already running results in no change.
  514. """
  515. service = self.create_service('web')
  516. next_number = service._next_container_number()
  517. container = service.create_container(number=next_number, quiet=True)
  518. container.start()
  519. self.assertTrue(container.is_running)
  520. self.assertEqual(len(service.containers()), 1)
  521. service.scale(1)
  522. self.assertEqual(len(service.containers()), 1)
  523. container.inspect()
  524. self.assertTrue(container.is_running)
  525. captured_output = mock_log.info.call_args[0]
  526. self.assertIn('Desired container number already achieved', captured_output)
  527. @patch('compose.service.log')
  528. def test_scale_with_custom_container_name_outputs_warning(self, mock_log):
  529. """
  530. Test that calling scale on a service that has a custom container name
  531. results in warning output.
  532. """
  533. service = self.create_service('web', container_name='custom-container')
  534. self.assertEqual(service.custom_container_name(), 'custom-container')
  535. service.scale(3)
  536. captured_output = mock_log.warn.call_args[0][0]
  537. self.assertEqual(len(service.containers()), 1)
  538. self.assertIn(
  539. "Remove the custom name to scale the service.",
  540. captured_output
  541. )
  542. def test_scale_sets_ports(self):
  543. service = self.create_service('web', ports=['8000'])
  544. service.scale(2)
  545. containers = service.containers()
  546. self.assertEqual(len(containers), 2)
  547. for container in containers:
  548. self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp'])
  549. def test_network_mode_none(self):
  550. service = self.create_service('web', net='none')
  551. container = create_and_start_container(service)
  552. self.assertEqual(container.get('HostConfig.NetworkMode'), 'none')
  553. def test_network_mode_bridged(self):
  554. service = self.create_service('web', net='bridge')
  555. container = create_and_start_container(service)
  556. self.assertEqual(container.get('HostConfig.NetworkMode'), 'bridge')
  557. def test_network_mode_host(self):
  558. service = self.create_service('web', net='host')
  559. container = create_and_start_container(service)
  560. self.assertEqual(container.get('HostConfig.NetworkMode'), 'host')
  561. def test_pid_mode_none_defined(self):
  562. service = self.create_service('web', pid=None)
  563. container = create_and_start_container(service)
  564. self.assertEqual(container.get('HostConfig.PidMode'), '')
  565. def test_pid_mode_host(self):
  566. service = self.create_service('web', pid='host')
  567. container = create_and_start_container(service)
  568. self.assertEqual(container.get('HostConfig.PidMode'), 'host')
  569. def test_dns_no_value(self):
  570. service = self.create_service('web')
  571. container = create_and_start_container(service)
  572. self.assertIsNone(container.get('HostConfig.Dns'))
  573. def test_dns_single_value(self):
  574. service = self.create_service('web', dns='8.8.8.8')
  575. container = create_and_start_container(service)
  576. self.assertEqual(container.get('HostConfig.Dns'), ['8.8.8.8'])
  577. def test_dns_list(self):
  578. service = self.create_service('web', dns=['8.8.8.8', '9.9.9.9'])
  579. container = create_and_start_container(service)
  580. self.assertEqual(container.get('HostConfig.Dns'), ['8.8.8.8', '9.9.9.9'])
  581. def test_restart_always_value(self):
  582. service = self.create_service('web', restart='always')
  583. container = create_and_start_container(service)
  584. self.assertEqual(container.get('HostConfig.RestartPolicy.Name'), 'always')
  585. def test_restart_on_failure_value(self):
  586. service = self.create_service('web', restart='on-failure:5')
  587. container = create_and_start_container(service)
  588. self.assertEqual(container.get('HostConfig.RestartPolicy.Name'), 'on-failure')
  589. self.assertEqual(container.get('HostConfig.RestartPolicy.MaximumRetryCount'), 5)
  590. def test_cap_add_list(self):
  591. service = self.create_service('web', cap_add=['SYS_ADMIN', 'NET_ADMIN'])
  592. container = create_and_start_container(service)
  593. self.assertEqual(container.get('HostConfig.CapAdd'), ['SYS_ADMIN', 'NET_ADMIN'])
  594. def test_cap_drop_list(self):
  595. service = self.create_service('web', cap_drop=['SYS_ADMIN', 'NET_ADMIN'])
  596. container = create_and_start_container(service)
  597. self.assertEqual(container.get('HostConfig.CapDrop'), ['SYS_ADMIN', 'NET_ADMIN'])
  598. def test_dns_search_no_value(self):
  599. service = self.create_service('web')
  600. container = create_and_start_container(service)
  601. self.assertIsNone(container.get('HostConfig.DnsSearch'))
  602. def test_dns_search_single_value(self):
  603. service = self.create_service('web', dns_search='example.com')
  604. container = create_and_start_container(service)
  605. self.assertEqual(container.get('HostConfig.DnsSearch'), ['example.com'])
  606. def test_dns_search_list(self):
  607. service = self.create_service('web', dns_search=['dc1.example.com', 'dc2.example.com'])
  608. container = create_and_start_container(service)
  609. self.assertEqual(container.get('HostConfig.DnsSearch'), ['dc1.example.com', 'dc2.example.com'])
  610. def test_working_dir_param(self):
  611. service = self.create_service('container', working_dir='/working/dir/sample')
  612. container = service.create_container()
  613. self.assertEqual(container.get('Config.WorkingDir'), '/working/dir/sample')
  614. def test_split_env(self):
  615. service = self.create_service('web', environment=['NORMAL=F1', 'CONTAINS_EQUALS=F=2', 'TRAILING_EQUALS='])
  616. env = create_and_start_container(service).environment
  617. for k, v in {'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''}.items():
  618. self.assertEqual(env[k], v)
  619. def test_env_from_file_combined_with_env(self):
  620. service = self.create_service('web', environment=['ONE=1', 'TWO=2', 'THREE=3'], env_file=['tests/fixtures/env/one.env', 'tests/fixtures/env/two.env'])
  621. env = create_and_start_container(service).environment
  622. for k, v in {'ONE': '1', 'TWO': '2', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'}.items():
  623. self.assertEqual(env[k], v)
  624. @patch.dict(os.environ)
  625. def test_resolve_env(self):
  626. os.environ['FILE_DEF'] = 'E1'
  627. os.environ['FILE_DEF_EMPTY'] = 'E2'
  628. os.environ['ENV_DEF'] = 'E3'
  629. service = self.create_service('web', environment={'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': None, 'NO_DEF': None})
  630. env = create_and_start_container(service).environment
  631. for k, v in {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}.items():
  632. self.assertEqual(env[k], v)
  633. def test_labels(self):
  634. labels_dict = {
  635. 'com.example.description': "Accounting webapp",
  636. 'com.example.department': "Finance",
  637. 'com.example.label-with-empty-value': "",
  638. }
  639. compose_labels = {
  640. LABEL_CONTAINER_NUMBER: '1',
  641. LABEL_ONE_OFF: 'False',
  642. LABEL_PROJECT: 'composetest',
  643. LABEL_SERVICE: 'web',
  644. LABEL_VERSION: __version__,
  645. }
  646. expected = dict(labels_dict, **compose_labels)
  647. service = self.create_service('web', labels=labels_dict)
  648. labels = create_and_start_container(service).labels.items()
  649. for pair in expected.items():
  650. self.assertIn(pair, labels)
  651. service.kill()
  652. service.remove_stopped()
  653. labels_list = ["%s=%s" % pair for pair in labels_dict.items()]
  654. service = self.create_service('web', labels=labels_list)
  655. labels = create_and_start_container(service).labels.items()
  656. for pair in expected.items():
  657. self.assertIn(pair, labels)
  658. def test_empty_labels(self):
  659. labels_list = ['foo', 'bar']
  660. service = self.create_service('web', labels=labels_list)
  661. labels = create_and_start_container(service).labels.items()
  662. for name in labels_list:
  663. self.assertIn((name, ''), labels)
  664. def test_custom_container_name(self):
  665. service = self.create_service('web', container_name='my-web-container')
  666. self.assertEqual(service.custom_container_name(), 'my-web-container')
  667. container = create_and_start_container(service)
  668. self.assertEqual(container.name, 'my-web-container')
  669. one_off_container = service.create_container(one_off=True)
  670. self.assertNotEqual(one_off_container.name, 'my-web-container')
  671. def test_log_drive_invalid(self):
  672. service = self.create_service('web', log_driver='xxx')
  673. self.assertRaises(ValueError, lambda: create_and_start_container(service))
  674. def test_log_drive_empty_default_jsonfile(self):
  675. service = self.create_service('web')
  676. log_config = create_and_start_container(service).log_config
  677. self.assertEqual('json-file', log_config['Type'])
  678. self.assertFalse(log_config['Config'])
  679. def test_log_drive_none(self):
  680. service = self.create_service('web', log_driver='none')
  681. log_config = create_and_start_container(service).log_config
  682. self.assertEqual('none', log_config['Type'])
  683. self.assertFalse(log_config['Config'])
  684. def test_devices(self):
  685. service = self.create_service('web', devices=["/dev/random:/dev/mapped-random"])
  686. device_config = create_and_start_container(service).get('HostConfig.Devices')
  687. device_dict = {
  688. 'PathOnHost': '/dev/random',
  689. 'CgroupPermissions': 'rwm',
  690. 'PathInContainer': '/dev/mapped-random'
  691. }
  692. self.assertEqual(1, len(device_config))
  693. self.assertDictEqual(device_dict, device_config[0])
  694. def test_duplicate_containers(self):
  695. service = self.create_service('web')
  696. options = service._get_container_create_options({}, 1)
  697. original = Container.create(service.client, **options)
  698. self.assertEqual(set(service.containers(stopped=True)), set([original]))
  699. self.assertEqual(set(service.duplicate_containers()), set())
  700. options['name'] = 'temporary_container_name'
  701. duplicate = Container.create(service.client, **options)
  702. self.assertEqual(set(service.containers(stopped=True)), set([original, duplicate]))
  703. self.assertEqual(set(service.duplicate_containers()), set([duplicate]))