service_test.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  1. from __future__ import absolute_import
  2. from __future__ import unicode_literals
  3. import docker
  4. import pytest
  5. from docker.errors import APIError
  6. from .. import mock
  7. from .. import unittest
  8. from compose.config.types import VolumeFromSpec
  9. from compose.config.types import VolumeSpec
  10. from compose.const import LABEL_CONFIG_HASH
  11. from compose.const import LABEL_ONE_OFF
  12. from compose.const import LABEL_PROJECT
  13. from compose.const import LABEL_SERVICE
  14. from compose.container import Container
  15. from compose.service import build_ulimits
  16. from compose.service import build_volume_binding
  17. from compose.service import BuildAction
  18. from compose.service import ContainerNetworkMode
  19. from compose.service import get_container_data_volumes
  20. from compose.service import ImageType
  21. from compose.service import merge_volume_bindings
  22. from compose.service import NeedsBuildError
  23. from compose.service import NetworkMode
  24. from compose.service import NoSuchImageError
  25. from compose.service import parse_repository_tag
  26. from compose.service import Service
  27. from compose.service import ServiceNetworkMode
  28. from compose.service import warn_on_masked_volume
  29. class ServiceTest(unittest.TestCase):
  30. def setUp(self):
  31. self.mock_client = mock.create_autospec(docker.Client)
  32. def test_containers(self):
  33. service = Service('db', self.mock_client, 'myproject', image='foo')
  34. self.mock_client.containers.return_value = []
  35. self.assertEqual(list(service.containers()), [])
  36. def test_containers_with_containers(self):
  37. self.mock_client.containers.return_value = [
  38. dict(Name=str(i), Image='foo', Id=i) for i in range(3)
  39. ]
  40. service = Service('db', self.mock_client, 'myproject', image='foo')
  41. self.assertEqual([c.id for c in service.containers()], list(range(3)))
  42. expected_labels = [
  43. '{0}=myproject'.format(LABEL_PROJECT),
  44. '{0}=db'.format(LABEL_SERVICE),
  45. '{0}=False'.format(LABEL_ONE_OFF),
  46. ]
  47. self.mock_client.containers.assert_called_once_with(
  48. all=False,
  49. filters={'label': expected_labels})
  50. def test_container_without_name(self):
  51. self.mock_client.containers.return_value = [
  52. {'Image': 'foo', 'Id': '1', 'Name': '1'},
  53. {'Image': 'foo', 'Id': '2', 'Name': None},
  54. {'Image': 'foo', 'Id': '3'},
  55. ]
  56. service = Service('db', self.mock_client, 'myproject', image='foo')
  57. self.assertEqual([c.id for c in service.containers()], ['1'])
  58. self.assertEqual(service._next_container_number(), 2)
  59. self.assertEqual(service.get_container(1).id, '1')
  60. def test_get_volumes_from_container(self):
  61. container_id = 'aabbccddee'
  62. service = Service(
  63. 'test',
  64. image='foo',
  65. volumes_from=[
  66. VolumeFromSpec(
  67. mock.Mock(id=container_id, spec=Container),
  68. 'rw',
  69. 'container')])
  70. self.assertEqual(service._get_volumes_from(), [container_id + ':rw'])
  71. def test_get_volumes_from_container_read_only(self):
  72. container_id = 'aabbccddee'
  73. service = Service(
  74. 'test',
  75. image='foo',
  76. volumes_from=[
  77. VolumeFromSpec(
  78. mock.Mock(id=container_id, spec=Container),
  79. 'ro',
  80. 'container')])
  81. self.assertEqual(service._get_volumes_from(), [container_id + ':ro'])
  82. def test_get_volumes_from_service_container_exists(self):
  83. container_ids = ['aabbccddee', '12345']
  84. from_service = mock.create_autospec(Service)
  85. from_service.containers.return_value = [
  86. mock.Mock(id=container_id, spec=Container)
  87. for container_id in container_ids
  88. ]
  89. service = Service(
  90. 'test',
  91. volumes_from=[VolumeFromSpec(from_service, 'rw', 'service')],
  92. image='foo')
  93. self.assertEqual(service._get_volumes_from(), [container_ids[0] + ":rw"])
  94. def test_get_volumes_from_service_container_exists_with_flags(self):
  95. for mode in ['ro', 'rw', 'z', 'rw,z', 'z,rw']:
  96. container_ids = ['aabbccddee:' + mode, '12345:' + mode]
  97. from_service = mock.create_autospec(Service)
  98. from_service.containers.return_value = [
  99. mock.Mock(id=container_id.split(':')[0], spec=Container)
  100. for container_id in container_ids
  101. ]
  102. service = Service(
  103. 'test',
  104. volumes_from=[VolumeFromSpec(from_service, mode, 'service')],
  105. image='foo')
  106. self.assertEqual(service._get_volumes_from(), [container_ids[0]])
  107. def test_get_volumes_from_service_no_container(self):
  108. container_id = 'abababab'
  109. from_service = mock.create_autospec(Service)
  110. from_service.containers.return_value = []
  111. from_service.create_container.return_value = mock.Mock(
  112. id=container_id,
  113. spec=Container)
  114. service = Service(
  115. 'test',
  116. image='foo',
  117. volumes_from=[VolumeFromSpec(from_service, 'rw', 'service')])
  118. self.assertEqual(service._get_volumes_from(), [container_id + ':rw'])
  119. from_service.create_container.assert_called_once_with()
  120. def test_split_domainname_none(self):
  121. service = Service('foo', image='foo', hostname='name', client=self.mock_client)
  122. opts = service._get_container_create_options({'image': 'foo'}, 1)
  123. self.assertEqual(opts['hostname'], 'name', 'hostname')
  124. self.assertFalse('domainname' in opts, 'domainname')
  125. def test_memory_swap_limit(self):
  126. self.mock_client.create_host_config.return_value = {}
  127. service = Service(
  128. name='foo',
  129. image='foo',
  130. hostname='name',
  131. client=self.mock_client,
  132. mem_limit=1000000000,
  133. memswap_limit=2000000000)
  134. service._get_container_create_options({'some': 'overrides'}, 1)
  135. self.assertTrue(self.mock_client.create_host_config.called)
  136. self.assertEqual(
  137. self.mock_client.create_host_config.call_args[1]['mem_limit'],
  138. 1000000000
  139. )
  140. self.assertEqual(
  141. self.mock_client.create_host_config.call_args[1]['memswap_limit'],
  142. 2000000000
  143. )
  144. def test_cgroup_parent(self):
  145. self.mock_client.create_host_config.return_value = {}
  146. service = Service(
  147. name='foo',
  148. image='foo',
  149. hostname='name',
  150. client=self.mock_client,
  151. cgroup_parent='test')
  152. service._get_container_create_options({'some': 'overrides'}, 1)
  153. self.assertTrue(self.mock_client.create_host_config.called)
  154. self.assertEqual(
  155. self.mock_client.create_host_config.call_args[1]['cgroup_parent'],
  156. 'test'
  157. )
  158. def test_log_opt(self):
  159. self.mock_client.create_host_config.return_value = {}
  160. log_opt = {'syslog-address': 'tcp://192.168.0.42:123'}
  161. logging = {'driver': 'syslog', 'options': log_opt}
  162. service = Service(
  163. name='foo',
  164. image='foo',
  165. hostname='name',
  166. client=self.mock_client,
  167. log_driver='syslog',
  168. logging=logging)
  169. service._get_container_create_options({'some': 'overrides'}, 1)
  170. self.assertTrue(self.mock_client.create_host_config.called)
  171. self.assertEqual(
  172. self.mock_client.create_host_config.call_args[1]['log_config'],
  173. {'Type': 'syslog', 'Config': {'syslog-address': 'tcp://192.168.0.42:123'}}
  174. )
  175. def test_split_domainname_fqdn(self):
  176. service = Service(
  177. 'foo',
  178. hostname='name.domain.tld',
  179. image='foo',
  180. client=self.mock_client)
  181. opts = service._get_container_create_options({'image': 'foo'}, 1)
  182. self.assertEqual(opts['hostname'], 'name', 'hostname')
  183. self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
  184. def test_split_domainname_both(self):
  185. service = Service(
  186. 'foo',
  187. hostname='name',
  188. image='foo',
  189. domainname='domain.tld',
  190. client=self.mock_client)
  191. opts = service._get_container_create_options({'image': 'foo'}, 1)
  192. self.assertEqual(opts['hostname'], 'name', 'hostname')
  193. self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
  194. def test_split_domainname_weird(self):
  195. service = Service(
  196. 'foo',
  197. hostname='name.sub',
  198. domainname='domain.tld',
  199. image='foo',
  200. client=self.mock_client)
  201. opts = service._get_container_create_options({'image': 'foo'}, 1)
  202. self.assertEqual(opts['hostname'], 'name.sub', 'hostname')
  203. self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
  204. def test_no_default_hostname_when_not_using_networking(self):
  205. service = Service(
  206. 'foo',
  207. image='foo',
  208. use_networking=False,
  209. client=self.mock_client,
  210. )
  211. opts = service._get_container_create_options({'image': 'foo'}, 1)
  212. self.assertIsNone(opts.get('hostname'))
  213. def test_get_container_create_options_with_name_option(self):
  214. service = Service(
  215. 'foo',
  216. image='foo',
  217. client=self.mock_client,
  218. container_name='foo1')
  219. name = 'the_new_name'
  220. opts = service._get_container_create_options(
  221. {'name': name},
  222. 1,
  223. one_off=True)
  224. self.assertEqual(opts['name'], name)
  225. def test_get_container_create_options_does_not_mutate_options(self):
  226. labels = {'thing': 'real'}
  227. environment = {'also': 'real'}
  228. service = Service(
  229. 'foo',
  230. image='foo',
  231. labels=dict(labels),
  232. client=self.mock_client,
  233. environment=dict(environment),
  234. )
  235. self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
  236. prev_container = mock.Mock(
  237. id='ababab',
  238. image_config={'ContainerConfig': {}})
  239. prev_container.get.return_value = None
  240. opts = service._get_container_create_options(
  241. {},
  242. 1,
  243. previous_container=prev_container)
  244. self.assertEqual(service.options['labels'], labels)
  245. self.assertEqual(service.options['environment'], environment)
  246. self.assertEqual(
  247. opts['labels'][LABEL_CONFIG_HASH],
  248. 'f8bfa1058ad1f4231372a0b1639f0dfdb574dafff4e8d7938049ae993f7cf1fc')
  249. assert opts['environment'] == ['also=real']
  250. def test_get_container_create_options_sets_affinity_with_binds(self):
  251. service = Service(
  252. 'foo',
  253. image='foo',
  254. client=self.mock_client,
  255. )
  256. self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
  257. prev_container = mock.Mock(
  258. id='ababab',
  259. image_config={'ContainerConfig': {'Volumes': ['/data']}})
  260. def container_get(key):
  261. return {
  262. 'Mounts': [
  263. {
  264. 'Destination': '/data',
  265. 'Source': '/some/path',
  266. 'Name': 'abab1234',
  267. },
  268. ]
  269. }.get(key, None)
  270. prev_container.get.side_effect = container_get
  271. opts = service._get_container_create_options(
  272. {},
  273. 1,
  274. previous_container=prev_container)
  275. assert opts['environment'] == ['affinity:container==ababab']
  276. def test_get_container_create_options_no_affinity_without_binds(self):
  277. service = Service('foo', image='foo', client=self.mock_client)
  278. self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
  279. prev_container = mock.Mock(
  280. id='ababab',
  281. image_config={'ContainerConfig': {}})
  282. prev_container.get.return_value = None
  283. opts = service._get_container_create_options(
  284. {},
  285. 1,
  286. previous_container=prev_container)
  287. assert opts['environment'] == []
  288. def test_get_container_not_found(self):
  289. self.mock_client.containers.return_value = []
  290. service = Service('foo', client=self.mock_client, image='foo')
  291. self.assertRaises(ValueError, service.get_container)
  292. @mock.patch('compose.service.Container', autospec=True)
  293. def test_get_container(self, mock_container_class):
  294. container_dict = dict(Name='default_foo_2')
  295. self.mock_client.containers.return_value = [container_dict]
  296. service = Service('foo', image='foo', client=self.mock_client)
  297. container = service.get_container(number=2)
  298. self.assertEqual(container, mock_container_class.from_ps.return_value)
  299. mock_container_class.from_ps.assert_called_once_with(
  300. self.mock_client, container_dict)
  301. @mock.patch('compose.service.log', autospec=True)
  302. def test_pull_image(self, mock_log):
  303. service = Service('foo', client=self.mock_client, image='someimage:sometag')
  304. service.pull()
  305. self.mock_client.pull.assert_called_once_with(
  306. 'someimage',
  307. tag='sometag',
  308. stream=True)
  309. mock_log.info.assert_called_once_with('Pulling foo (someimage:sometag)...')
  310. def test_pull_image_no_tag(self):
  311. service = Service('foo', client=self.mock_client, image='ababab')
  312. service.pull()
  313. self.mock_client.pull.assert_called_once_with(
  314. 'ababab',
  315. tag='latest',
  316. stream=True)
  317. @mock.patch('compose.service.log', autospec=True)
  318. def test_pull_image_digest(self, mock_log):
  319. service = Service('foo', client=self.mock_client, image='someimage@sha256:1234')
  320. service.pull()
  321. self.mock_client.pull.assert_called_once_with(
  322. 'someimage',
  323. tag='sha256:1234',
  324. stream=True)
  325. mock_log.info.assert_called_once_with('Pulling foo (someimage@sha256:1234)...')
  326. @mock.patch('compose.service.Container', autospec=True)
  327. def test_recreate_container(self, _):
  328. mock_container = mock.create_autospec(Container)
  329. service = Service('foo', client=self.mock_client, image='someimage')
  330. service.image = lambda: {'Id': 'abc123'}
  331. new_container = service.recreate_container(mock_container)
  332. mock_container.stop.assert_called_once_with(timeout=10)
  333. mock_container.rename_to_tmp_name.assert_called_once_with()
  334. new_container.start.assert_called_once_with()
  335. mock_container.remove.assert_called_once_with()
  336. @mock.patch('compose.service.Container', autospec=True)
  337. def test_recreate_container_with_timeout(self, _):
  338. mock_container = mock.create_autospec(Container)
  339. self.mock_client.inspect_image.return_value = {'Id': 'abc123'}
  340. service = Service('foo', client=self.mock_client, image='someimage')
  341. service.recreate_container(mock_container, timeout=1)
  342. mock_container.stop.assert_called_once_with(timeout=1)
  343. def test_parse_repository_tag(self):
  344. self.assertEqual(parse_repository_tag("root"), ("root", "", ":"))
  345. self.assertEqual(parse_repository_tag("root:tag"), ("root", "tag", ":"))
  346. self.assertEqual(parse_repository_tag("user/repo"), ("user/repo", "", ":"))
  347. self.assertEqual(parse_repository_tag("user/repo:tag"), ("user/repo", "tag", ":"))
  348. self.assertEqual(parse_repository_tag("url:5000/repo"), ("url:5000/repo", "", ":"))
  349. self.assertEqual(
  350. parse_repository_tag("url:5000/repo:tag"),
  351. ("url:5000/repo", "tag", ":"))
  352. self.assertEqual(
  353. parse_repository_tag("root@sha256:digest"),
  354. ("root", "sha256:digest", "@"))
  355. self.assertEqual(
  356. parse_repository_tag("user/repo@sha256:digest"),
  357. ("user/repo", "sha256:digest", "@"))
  358. self.assertEqual(
  359. parse_repository_tag("url:5000/repo@sha256:digest"),
  360. ("url:5000/repo", "sha256:digest", "@"))
  361. def test_create_container_with_build(self):
  362. service = Service('foo', client=self.mock_client, build={'context': '.'})
  363. self.mock_client.inspect_image.side_effect = [
  364. NoSuchImageError,
  365. {'Id': 'abc123'},
  366. ]
  367. self.mock_client.build.return_value = [
  368. '{"stream": "Successfully built abcd"}',
  369. ]
  370. with mock.patch('compose.service.log', autospec=True) as mock_log:
  371. service.create_container(do_build=BuildAction.none)
  372. assert mock_log.warn.called
  373. _, args, _ = mock_log.warn.mock_calls[0]
  374. assert 'was built because it did not already exist' in args[0]
  375. self.mock_client.build.assert_called_once_with(
  376. tag='default_foo',
  377. dockerfile=None,
  378. stream=True,
  379. path='.',
  380. pull=False,
  381. forcerm=False,
  382. nocache=False,
  383. rm=True,
  384. buildargs=None,
  385. )
  386. def test_create_container_no_build(self):
  387. service = Service('foo', client=self.mock_client, build={'context': '.'})
  388. self.mock_client.inspect_image.return_value = {'Id': 'abc123'}
  389. service.create_container(do_build=BuildAction.skip)
  390. self.assertFalse(self.mock_client.build.called)
  391. def test_create_container_no_build_but_needs_build(self):
  392. service = Service('foo', client=self.mock_client, build={'context': '.'})
  393. self.mock_client.inspect_image.side_effect = NoSuchImageError
  394. with pytest.raises(NeedsBuildError):
  395. service.create_container(do_build=BuildAction.skip)
  396. def test_create_container_force_build(self):
  397. service = Service('foo', client=self.mock_client, build={'context': '.'})
  398. self.mock_client.inspect_image.return_value = {'Id': 'abc123'}
  399. self.mock_client.build.return_value = [
  400. '{"stream": "Successfully built abcd"}',
  401. ]
  402. with mock.patch('compose.service.log', autospec=True) as mock_log:
  403. service.create_container(do_build=BuildAction.force)
  404. assert not mock_log.warn.called
  405. self.mock_client.build.assert_called_once_with(
  406. tag='default_foo',
  407. dockerfile=None,
  408. stream=True,
  409. path='.',
  410. pull=False,
  411. forcerm=False,
  412. nocache=False,
  413. rm=True,
  414. buildargs=None,
  415. )
  416. def test_build_does_not_pull(self):
  417. self.mock_client.build.return_value = [
  418. b'{"stream": "Successfully built 12345"}',
  419. ]
  420. service = Service('foo', client=self.mock_client, build={'context': '.'})
  421. service.build()
  422. self.assertEqual(self.mock_client.build.call_count, 1)
  423. self.assertFalse(self.mock_client.build.call_args[1]['pull'])
  424. def test_config_dict(self):
  425. self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
  426. service = Service(
  427. 'foo',
  428. image='example.com/foo',
  429. client=self.mock_client,
  430. network_mode=ServiceNetworkMode(Service('other')),
  431. links=[(Service('one'), 'one')],
  432. volumes_from=[VolumeFromSpec(Service('two'), 'rw', 'service')])
  433. config_dict = service.config_dict()
  434. expected = {
  435. 'image_id': 'abcd',
  436. 'options': {'image': 'example.com/foo'},
  437. 'links': [('one', 'one')],
  438. 'net': 'other',
  439. 'networks': [],
  440. 'volumes_from': [('two', 'rw')],
  441. }
  442. assert config_dict == expected
  443. def test_config_dict_with_network_mode_from_container(self):
  444. self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
  445. container = Container(
  446. self.mock_client,
  447. {'Id': 'aaabbb', 'Name': '/foo_1'})
  448. service = Service(
  449. 'foo',
  450. image='example.com/foo',
  451. client=self.mock_client,
  452. network_mode=ContainerNetworkMode(container))
  453. config_dict = service.config_dict()
  454. expected = {
  455. 'image_id': 'abcd',
  456. 'options': {'image': 'example.com/foo'},
  457. 'links': [],
  458. 'networks': [],
  459. 'net': 'aaabbb',
  460. 'volumes_from': [],
  461. }
  462. assert config_dict == expected
  463. def test_remove_image_none(self):
  464. web = Service('web', image='example', client=self.mock_client)
  465. assert not web.remove_image(ImageType.none)
  466. assert not self.mock_client.remove_image.called
  467. def test_remove_image_local_with_image_name_doesnt_remove(self):
  468. web = Service('web', image='example', client=self.mock_client)
  469. assert not web.remove_image(ImageType.local)
  470. assert not self.mock_client.remove_image.called
  471. def test_remove_image_local_without_image_name_does_remove(self):
  472. web = Service('web', build='.', client=self.mock_client)
  473. assert web.remove_image(ImageType.local)
  474. self.mock_client.remove_image.assert_called_once_with(web.image_name)
  475. def test_remove_image_all_does_remove(self):
  476. web = Service('web', image='example', client=self.mock_client)
  477. assert web.remove_image(ImageType.all)
  478. self.mock_client.remove_image.assert_called_once_with(web.image_name)
  479. def test_remove_image_with_error(self):
  480. self.mock_client.remove_image.side_effect = error = APIError(
  481. message="testing",
  482. response={},
  483. explanation="Boom")
  484. web = Service('web', image='example', client=self.mock_client)
  485. with mock.patch('compose.service.log', autospec=True) as mock_log:
  486. assert not web.remove_image(ImageType.all)
  487. mock_log.error.assert_called_once_with(
  488. "Failed to remove image for service %s: %s", web.name, error)
  489. def test_specifies_host_port_with_no_ports(self):
  490. service = Service(
  491. 'foo',
  492. image='foo')
  493. self.assertEqual(service.specifies_host_port(), False)
  494. def test_specifies_host_port_with_container_port(self):
  495. service = Service(
  496. 'foo',
  497. image='foo',
  498. ports=["2000"])
  499. self.assertEqual(service.specifies_host_port(), False)
  500. def test_specifies_host_port_with_host_port(self):
  501. service = Service(
  502. 'foo',
  503. image='foo',
  504. ports=["1000:2000"])
  505. self.assertEqual(service.specifies_host_port(), True)
  506. def test_specifies_host_port_with_host_ip_no_port(self):
  507. service = Service(
  508. 'foo',
  509. image='foo',
  510. ports=["127.0.0.1::2000"])
  511. self.assertEqual(service.specifies_host_port(), False)
  512. def test_specifies_host_port_with_host_ip_and_port(self):
  513. service = Service(
  514. 'foo',
  515. image='foo',
  516. ports=["127.0.0.1:1000:2000"])
  517. self.assertEqual(service.specifies_host_port(), True)
  518. def test_specifies_host_port_with_container_port_range(self):
  519. service = Service(
  520. 'foo',
  521. image='foo',
  522. ports=["2000-3000"])
  523. self.assertEqual(service.specifies_host_port(), False)
  524. def test_specifies_host_port_with_host_port_range(self):
  525. service = Service(
  526. 'foo',
  527. image='foo',
  528. ports=["1000-2000:2000-3000"])
  529. self.assertEqual(service.specifies_host_port(), True)
  530. def test_specifies_host_port_with_host_ip_no_port_range(self):
  531. service = Service(
  532. 'foo',
  533. image='foo',
  534. ports=["127.0.0.1::2000-3000"])
  535. self.assertEqual(service.specifies_host_port(), False)
  536. def test_specifies_host_port_with_host_ip_and_port_range(self):
  537. service = Service(
  538. 'foo',
  539. image='foo',
  540. ports=["127.0.0.1:1000-2000:2000-3000"])
  541. self.assertEqual(service.specifies_host_port(), True)
  542. def test_image_name_from_config(self):
  543. image_name = 'example/web:latest'
  544. service = Service('foo', image=image_name)
  545. assert service.image_name == image_name
  546. def test_image_name_default(self):
  547. service = Service('foo', project='testing')
  548. assert service.image_name == 'testing_foo'
  549. def sort_by_name(dictionary_list):
  550. return sorted(dictionary_list, key=lambda k: k['name'])
  551. class BuildUlimitsTestCase(unittest.TestCase):
  552. def test_build_ulimits_with_dict(self):
  553. ulimits = build_ulimits(
  554. {
  555. 'nofile': {'soft': 10000, 'hard': 20000},
  556. 'nproc': {'soft': 65535, 'hard': 65535}
  557. }
  558. )
  559. expected = [
  560. {'name': 'nofile', 'soft': 10000, 'hard': 20000},
  561. {'name': 'nproc', 'soft': 65535, 'hard': 65535}
  562. ]
  563. assert sort_by_name(ulimits) == sort_by_name(expected)
  564. def test_build_ulimits_with_ints(self):
  565. ulimits = build_ulimits({'nofile': 20000, 'nproc': 65535})
  566. expected = [
  567. {'name': 'nofile', 'soft': 20000, 'hard': 20000},
  568. {'name': 'nproc', 'soft': 65535, 'hard': 65535}
  569. ]
  570. assert sort_by_name(ulimits) == sort_by_name(expected)
  571. def test_build_ulimits_with_integers_and_dicts(self):
  572. ulimits = build_ulimits(
  573. {
  574. 'nproc': 65535,
  575. 'nofile': {'soft': 10000, 'hard': 20000}
  576. }
  577. )
  578. expected = [
  579. {'name': 'nofile', 'soft': 10000, 'hard': 20000},
  580. {'name': 'nproc', 'soft': 65535, 'hard': 65535}
  581. ]
  582. assert sort_by_name(ulimits) == sort_by_name(expected)
  583. class NetTestCase(unittest.TestCase):
  584. def test_network_mode(self):
  585. network_mode = NetworkMode('host')
  586. self.assertEqual(network_mode.id, 'host')
  587. self.assertEqual(network_mode.mode, 'host')
  588. self.assertEqual(network_mode.service_name, None)
  589. def test_network_mode_container(self):
  590. container_id = 'abcd'
  591. network_mode = ContainerNetworkMode(Container(None, {'Id': container_id}))
  592. self.assertEqual(network_mode.id, container_id)
  593. self.assertEqual(network_mode.mode, 'container:' + container_id)
  594. self.assertEqual(network_mode.service_name, None)
  595. def test_network_mode_service(self):
  596. container_id = 'bbbb'
  597. service_name = 'web'
  598. mock_client = mock.create_autospec(docker.Client)
  599. mock_client.containers.return_value = [
  600. {'Id': container_id, 'Name': container_id, 'Image': 'abcd'},
  601. ]
  602. service = Service(name=service_name, client=mock_client)
  603. network_mode = ServiceNetworkMode(service)
  604. self.assertEqual(network_mode.id, service_name)
  605. self.assertEqual(network_mode.mode, 'container:' + container_id)
  606. self.assertEqual(network_mode.service_name, service_name)
  607. def test_network_mode_service_no_containers(self):
  608. service_name = 'web'
  609. mock_client = mock.create_autospec(docker.Client)
  610. mock_client.containers.return_value = []
  611. service = Service(name=service_name, client=mock_client)
  612. network_mode = ServiceNetworkMode(service)
  613. self.assertEqual(network_mode.id, service_name)
  614. self.assertEqual(network_mode.mode, None)
  615. self.assertEqual(network_mode.service_name, service_name)
  616. def build_mount(destination, source, mode='rw'):
  617. return {'Source': source, 'Destination': destination, 'Mode': mode}
  618. class ServiceVolumesTest(unittest.TestCase):
  619. def setUp(self):
  620. self.mock_client = mock.create_autospec(docker.Client)
  621. def test_build_volume_binding(self):
  622. binding = build_volume_binding(VolumeSpec.parse('/outside:/inside'))
  623. assert binding == ('/inside', '/outside:/inside:rw')
  624. def test_get_container_data_volumes(self):
  625. options = [VolumeSpec.parse(v) for v in [
  626. '/host/volume:/host/volume:ro',
  627. '/new/volume',
  628. '/existing/volume',
  629. 'named:/named/vol',
  630. ]]
  631. self.mock_client.inspect_image.return_value = {
  632. 'ContainerConfig': {
  633. 'Volumes': {
  634. '/mnt/image/data': {},
  635. }
  636. }
  637. }
  638. container = Container(self.mock_client, {
  639. 'Image': 'ababab',
  640. 'Mounts': [
  641. {
  642. 'Source': '/host/volume',
  643. 'Destination': '/host/volume',
  644. 'Mode': '',
  645. 'RW': True,
  646. 'Name': 'hostvolume',
  647. }, {
  648. 'Source': '/var/lib/docker/aaaaaaaa',
  649. 'Destination': '/existing/volume',
  650. 'Mode': '',
  651. 'RW': True,
  652. 'Name': 'existingvolume',
  653. }, {
  654. 'Source': '/var/lib/docker/bbbbbbbb',
  655. 'Destination': '/removed/volume',
  656. 'Mode': '',
  657. 'RW': True,
  658. 'Name': 'removedvolume',
  659. }, {
  660. 'Source': '/var/lib/docker/cccccccc',
  661. 'Destination': '/mnt/image/data',
  662. 'Mode': '',
  663. 'RW': True,
  664. 'Name': 'imagedata',
  665. },
  666. ]
  667. }, has_been_inspected=True)
  668. expected = [
  669. VolumeSpec.parse('existingvolume:/existing/volume:rw'),
  670. VolumeSpec.parse('imagedata:/mnt/image/data:rw'),
  671. ]
  672. volumes = get_container_data_volumes(container, options)
  673. assert sorted(volumes) == sorted(expected)
  674. def test_merge_volume_bindings(self):
  675. options = [
  676. VolumeSpec.parse('/host/volume:/host/volume:ro'),
  677. VolumeSpec.parse('/host/rw/volume:/host/rw/volume'),
  678. VolumeSpec.parse('/new/volume'),
  679. VolumeSpec.parse('/existing/volume'),
  680. ]
  681. self.mock_client.inspect_image.return_value = {
  682. 'ContainerConfig': {'Volumes': {}}
  683. }
  684. previous_container = Container(self.mock_client, {
  685. 'Id': 'cdefab',
  686. 'Image': 'ababab',
  687. 'Mounts': [{
  688. 'Source': '/var/lib/docker/aaaaaaaa',
  689. 'Destination': '/existing/volume',
  690. 'Mode': '',
  691. 'RW': True,
  692. 'Name': 'existingvolume',
  693. }],
  694. }, has_been_inspected=True)
  695. expected = [
  696. '/host/volume:/host/volume:ro',
  697. '/host/rw/volume:/host/rw/volume:rw',
  698. 'existingvolume:/existing/volume:rw',
  699. ]
  700. binds, affinity = merge_volume_bindings(options, previous_container)
  701. assert sorted(binds) == sorted(expected)
  702. assert affinity == {'affinity:container': '=cdefab'}
  703. def test_mount_same_host_path_to_two_volumes(self):
  704. service = Service(
  705. 'web',
  706. image='busybox',
  707. volumes=[
  708. VolumeSpec.parse('/host/path:/data1'),
  709. VolumeSpec.parse('/host/path:/data2'),
  710. ],
  711. client=self.mock_client,
  712. )
  713. self.mock_client.inspect_image.return_value = {
  714. 'Id': 'ababab',
  715. 'ContainerConfig': {
  716. 'Volumes': {}
  717. }
  718. }
  719. service._get_container_create_options(
  720. override_options={},
  721. number=1,
  722. )
  723. self.assertEqual(
  724. set(self.mock_client.create_host_config.call_args[1]['binds']),
  725. set([
  726. '/host/path:/data1:rw',
  727. '/host/path:/data2:rw',
  728. ]),
  729. )
  730. def test_get_container_create_options_with_different_host_path_in_container_json(self):
  731. service = Service(
  732. 'web',
  733. image='busybox',
  734. volumes=[VolumeSpec.parse('/host/path:/data')],
  735. client=self.mock_client,
  736. )
  737. volume_name = 'abcdefff1234'
  738. self.mock_client.inspect_image.return_value = {
  739. 'Id': 'ababab',
  740. 'ContainerConfig': {
  741. 'Volumes': {
  742. '/data': {},
  743. }
  744. }
  745. }
  746. self.mock_client.inspect_container.return_value = {
  747. 'Id': '123123123',
  748. 'Image': 'ababab',
  749. 'Mounts': [
  750. {
  751. 'Destination': '/data',
  752. 'Source': '/mnt/sda1/host/path',
  753. 'Mode': '',
  754. 'RW': True,
  755. 'Driver': 'local',
  756. 'Name': volume_name,
  757. },
  758. ]
  759. }
  760. service._get_container_create_options(
  761. override_options={},
  762. number=1,
  763. previous_container=Container(self.mock_client, {'Id': '123123123'}),
  764. )
  765. assert (
  766. self.mock_client.create_host_config.call_args[1]['binds'] ==
  767. ['{}:/data:rw'.format(volume_name)]
  768. )
  769. def test_warn_on_masked_volume_no_warning_when_no_container_volumes(self):
  770. volumes_option = [VolumeSpec('/home/user', '/path', 'rw')]
  771. container_volumes = []
  772. service = 'service_name'
  773. with mock.patch('compose.service.log', autospec=True) as mock_log:
  774. warn_on_masked_volume(volumes_option, container_volumes, service)
  775. assert not mock_log.warn.called
  776. def test_warn_on_masked_volume_when_masked(self):
  777. volumes_option = [VolumeSpec('/home/user', '/path', 'rw')]
  778. container_volumes = [
  779. VolumeSpec('/var/lib/docker/path', '/path', 'rw'),
  780. VolumeSpec('/var/lib/docker/path', '/other', 'rw'),
  781. ]
  782. service = 'service_name'
  783. with mock.patch('compose.service.log', autospec=True) as mock_log:
  784. warn_on_masked_volume(volumes_option, container_volumes, service)
  785. mock_log.warn.assert_called_once_with(mock.ANY)
  786. def test_warn_on_masked_no_warning_with_same_path(self):
  787. volumes_option = [VolumeSpec('/home/user', '/path', 'rw')]
  788. container_volumes = [VolumeSpec('/home/user', '/path', 'rw')]
  789. service = 'service_name'
  790. with mock.patch('compose.service.log', autospec=True) as mock_log:
  791. warn_on_masked_volume(volumes_option, container_volumes, service)
  792. assert not mock_log.warn.called
  793. def test_warn_on_masked_no_warning_with_container_only_option(self):
  794. volumes_option = [VolumeSpec(None, '/path', 'rw')]
  795. container_volumes = [
  796. VolumeSpec('/var/lib/docker/volume/path', '/path', 'rw')
  797. ]
  798. service = 'service_name'
  799. with mock.patch('compose.service.log', autospec=True) as mock_log:
  800. warn_on_masked_volume(volumes_option, container_volumes, service)
  801. assert not mock_log.warn.called
  802. def test_create_with_special_volume_mode(self):
  803. self.mock_client.inspect_image.return_value = {'Id': 'imageid'}
  804. self.mock_client.create_container.return_value = {'Id': 'containerid'}
  805. volume = '/tmp:/foo:z'
  806. Service(
  807. 'web',
  808. client=self.mock_client,
  809. image='busybox',
  810. volumes=[VolumeSpec.parse(volume)],
  811. ).create_container()
  812. assert self.mock_client.create_container.call_count == 1
  813. self.assertEqual(
  814. self.mock_client.create_host_config.call_args[1]['binds'],
  815. [volume])