service_test.py 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550
  1. import docker
  2. import pytest
  3. from docker.constants import DEFAULT_DOCKER_API_VERSION
  4. from docker.errors import APIError
  5. from docker.errors import ImageNotFound
  6. from docker.errors import NotFound
  7. from .. import mock
  8. from .. import unittest
  9. from compose.config.errors import DependencyError
  10. from compose.config.types import MountSpec
  11. from compose.config.types import ServicePort
  12. from compose.config.types import ServiceSecret
  13. from compose.config.types import VolumeFromSpec
  14. from compose.config.types import VolumeSpec
  15. from compose.const import API_VERSIONS
  16. from compose.const import LABEL_CONFIG_HASH
  17. from compose.const import LABEL_ONE_OFF
  18. from compose.const import LABEL_PROJECT
  19. from compose.const import LABEL_SERVICE
  20. from compose.const import SECRETS_PATH
  21. from compose.const import WINDOWS_LONGPATH_PREFIX
  22. from compose.container import Container
  23. from compose.errors import OperationFailedError
  24. from compose.parallel import ParallelStreamWriter
  25. from compose.project import OneOffFilter
  26. from compose.service import build_ulimits
  27. from compose.service import build_volume_binding
  28. from compose.service import BuildAction
  29. from compose.service import ContainerNetworkMode
  30. from compose.service import format_environment
  31. from compose.service import formatted_ports
  32. from compose.service import get_container_data_volumes
  33. from compose.service import ImageType
  34. from compose.service import merge_volume_bindings
  35. from compose.service import NeedsBuildError
  36. from compose.service import NetworkMode
  37. from compose.service import NoSuchImageError
  38. from compose.service import parse_repository_tag
  39. from compose.service import rewrite_build_path
  40. from compose.service import Service
  41. from compose.service import ServiceNetworkMode
  42. from compose.service import warn_on_masked_volume
  43. class ServiceTest(unittest.TestCase):
  44. def setUp(self):
  45. self.mock_client = mock.create_autospec(docker.APIClient)
  46. self.mock_client.api_version = DEFAULT_DOCKER_API_VERSION
  47. self.mock_client._general_configs = {}
  48. def test_containers(self):
  49. service = Service('db', self.mock_client, 'myproject', image='foo')
  50. self.mock_client.containers.return_value = []
  51. assert list(service.containers()) == []
  52. def test_containers_with_containers(self):
  53. self.mock_client.containers.return_value = [
  54. dict(Name=str(i), Image='foo', Id=i) for i in range(3)
  55. ]
  56. service = Service('db', self.mock_client, 'myproject', image='foo')
  57. assert [c.id for c in service.containers()] == list(range(3))
  58. expected_labels = [
  59. '{0}=myproject'.format(LABEL_PROJECT),
  60. '{0}=db'.format(LABEL_SERVICE),
  61. '{0}=False'.format(LABEL_ONE_OFF),
  62. ]
  63. self.mock_client.containers.assert_called_once_with(
  64. all=False,
  65. filters={'label': expected_labels})
  66. def test_container_without_name(self):
  67. self.mock_client.containers.return_value = [
  68. {'Image': 'foo', 'Id': '1', 'Name': '1'},
  69. {'Image': 'foo', 'Id': '2', 'Name': None},
  70. {'Image': 'foo', 'Id': '3'},
  71. ]
  72. service = Service('db', self.mock_client, 'myproject', image='foo')
  73. assert [c.id for c in service.containers()] == ['1']
  74. assert service._next_container_number() == 2
  75. assert service.get_container(1).id == '1'
  76. def test_get_volumes_from_container(self):
  77. container_id = 'aabbccddee'
  78. service = Service(
  79. 'test',
  80. image='foo',
  81. volumes_from=[
  82. VolumeFromSpec(
  83. mock.Mock(id=container_id, spec=Container),
  84. 'rw',
  85. 'container')])
  86. assert service._get_volumes_from() == [container_id + ':rw']
  87. def test_get_volumes_from_container_read_only(self):
  88. container_id = 'aabbccddee'
  89. service = Service(
  90. 'test',
  91. image='foo',
  92. volumes_from=[
  93. VolumeFromSpec(
  94. mock.Mock(id=container_id, spec=Container),
  95. 'ro',
  96. 'container')])
  97. assert service._get_volumes_from() == [container_id + ':ro']
  98. def test_get_volumes_from_service_container_exists(self):
  99. container_ids = ['aabbccddee', '12345']
  100. from_service = mock.create_autospec(Service)
  101. from_service.containers.return_value = [
  102. mock.Mock(id=container_id, spec=Container)
  103. for container_id in container_ids
  104. ]
  105. service = Service(
  106. 'test',
  107. volumes_from=[VolumeFromSpec(from_service, 'rw', 'service')],
  108. image='foo')
  109. assert service._get_volumes_from() == [container_ids[0] + ":rw"]
  110. def test_get_volumes_from_service_container_exists_with_flags(self):
  111. for mode in ['ro', 'rw', 'z', 'rw,z', 'z,rw']:
  112. container_ids = ['aabbccddee:' + mode, '12345:' + mode]
  113. from_service = mock.create_autospec(Service)
  114. from_service.containers.return_value = [
  115. mock.Mock(id=container_id.split(':')[0], spec=Container)
  116. for container_id in container_ids
  117. ]
  118. service = Service(
  119. 'test',
  120. volumes_from=[VolumeFromSpec(from_service, mode, 'service')],
  121. image='foo')
  122. assert service._get_volumes_from() == [container_ids[0]]
  123. def test_get_volumes_from_service_no_container(self):
  124. container_id = 'abababab'
  125. from_service = mock.create_autospec(Service)
  126. from_service.containers.return_value = []
  127. from_service.create_container.return_value = mock.Mock(
  128. id=container_id,
  129. spec=Container)
  130. service = Service(
  131. 'test',
  132. image='foo',
  133. volumes_from=[VolumeFromSpec(from_service, 'rw', 'service')])
  134. assert service._get_volumes_from() == [container_id + ':rw']
  135. from_service.create_container.assert_called_once_with()
  136. def test_memory_swap_limit(self):
  137. self.mock_client.create_host_config.return_value = {}
  138. service = Service(
  139. name='foo',
  140. image='foo',
  141. hostname='name',
  142. client=self.mock_client,
  143. mem_limit=1000000000,
  144. memswap_limit=2000000000)
  145. service._get_container_create_options({'some': 'overrides'}, 1)
  146. assert self.mock_client.create_host_config.called
  147. assert self.mock_client.create_host_config.call_args[1]['mem_limit'] == 1000000000
  148. assert self.mock_client.create_host_config.call_args[1]['memswap_limit'] == 2000000000
  149. def test_self_reference_external_link(self):
  150. service = Service(
  151. name='foo',
  152. external_links=['default_foo_1']
  153. )
  154. with pytest.raises(DependencyError):
  155. service.get_container_name('foo', 1)
  156. def test_mem_reservation(self):
  157. self.mock_client.create_host_config.return_value = {}
  158. service = Service(
  159. name='foo',
  160. image='foo',
  161. hostname='name',
  162. client=self.mock_client,
  163. mem_reservation='512m'
  164. )
  165. service._get_container_create_options({'some': 'overrides'}, 1)
  166. assert self.mock_client.create_host_config.called is True
  167. assert self.mock_client.create_host_config.call_args[1]['mem_reservation'] == '512m'
  168. def test_cgroup_parent(self):
  169. self.mock_client.create_host_config.return_value = {}
  170. service = Service(
  171. name='foo',
  172. image='foo',
  173. hostname='name',
  174. client=self.mock_client,
  175. cgroup_parent='test')
  176. service._get_container_create_options({'some': 'overrides'}, 1)
  177. assert self.mock_client.create_host_config.called
  178. assert self.mock_client.create_host_config.call_args[1]['cgroup_parent'] == 'test'
  179. def test_log_opt(self):
  180. self.mock_client.create_host_config.return_value = {}
  181. log_opt = {'syslog-address': 'tcp://192.168.0.42:123'}
  182. logging = {'driver': 'syslog', 'options': log_opt}
  183. service = Service(
  184. name='foo',
  185. image='foo',
  186. hostname='name',
  187. client=self.mock_client,
  188. log_driver='syslog',
  189. logging=logging)
  190. service._get_container_create_options({'some': 'overrides'}, 1)
  191. assert self.mock_client.create_host_config.called
  192. assert self.mock_client.create_host_config.call_args[1]['log_config'] == {
  193. 'Type': 'syslog', 'Config': {'syslog-address': 'tcp://192.168.0.42:123'}
  194. }
  195. def test_stop_grace_period(self):
  196. self.mock_client.api_version = '1.25'
  197. self.mock_client.create_host_config.return_value = {}
  198. service = Service(
  199. 'foo',
  200. image='foo',
  201. client=self.mock_client,
  202. stop_grace_period="1m35s")
  203. opts = service._get_container_create_options({'image': 'foo'}, 1)
  204. assert opts['stop_timeout'] == 95
  205. def test_split_domainname_none(self):
  206. service = Service(
  207. 'foo',
  208. image='foo',
  209. hostname='name.domain.tld',
  210. client=self.mock_client)
  211. opts = service._get_container_create_options({'image': 'foo'}, 1)
  212. assert opts['hostname'] == 'name.domain.tld', 'hostname'
  213. assert not ('domainname' in opts), 'domainname'
  214. def test_split_domainname_fqdn(self):
  215. self.mock_client.api_version = '1.22'
  216. service = Service(
  217. 'foo',
  218. hostname='name.domain.tld',
  219. image='foo',
  220. client=self.mock_client)
  221. opts = service._get_container_create_options({'image': 'foo'}, 1)
  222. assert opts['hostname'] == 'name', 'hostname'
  223. assert opts['domainname'] == 'domain.tld', 'domainname'
  224. def test_split_domainname_both(self):
  225. self.mock_client.api_version = '1.22'
  226. service = Service(
  227. 'foo',
  228. hostname='name',
  229. image='foo',
  230. domainname='domain.tld',
  231. client=self.mock_client)
  232. opts = service._get_container_create_options({'image': 'foo'}, 1)
  233. assert opts['hostname'] == 'name', 'hostname'
  234. assert opts['domainname'] == 'domain.tld', 'domainname'
  235. def test_split_domainname_weird(self):
  236. self.mock_client.api_version = '1.22'
  237. service = Service(
  238. 'foo',
  239. hostname='name.sub',
  240. domainname='domain.tld',
  241. image='foo',
  242. client=self.mock_client)
  243. opts = service._get_container_create_options({'image': 'foo'}, 1)
  244. assert opts['hostname'] == 'name.sub', 'hostname'
  245. assert opts['domainname'] == 'domain.tld', 'domainname'
  246. def test_no_default_hostname_when_not_using_networking(self):
  247. service = Service(
  248. 'foo',
  249. image='foo',
  250. use_networking=False,
  251. client=self.mock_client,
  252. )
  253. opts = service._get_container_create_options({'image': 'foo'}, 1)
  254. assert opts.get('hostname') is None
  255. def test_get_container_create_options_with_name_option(self):
  256. service = Service(
  257. 'foo',
  258. image='foo',
  259. client=self.mock_client,
  260. container_name='foo1')
  261. name = 'the_new_name'
  262. opts = service._get_container_create_options(
  263. {'name': name},
  264. 1,
  265. one_off=OneOffFilter.only)
  266. assert opts['name'] == name
  267. def test_get_container_create_options_does_not_mutate_options(self):
  268. labels = {'thing': 'real'}
  269. environment = {'also': 'real'}
  270. service = Service(
  271. 'foo',
  272. image='foo',
  273. labels=dict(labels),
  274. client=self.mock_client,
  275. environment=dict(environment),
  276. )
  277. self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
  278. prev_container = mock.Mock(
  279. id='ababab',
  280. image_config={'ContainerConfig': {}}
  281. )
  282. prev_container.full_slug = 'abcdefff1234'
  283. prev_container.get.return_value = None
  284. opts = service._get_container_create_options(
  285. {}, 1, previous_container=prev_container
  286. )
  287. assert service.options['labels'] == labels
  288. assert service.options['environment'] == environment
  289. assert opts['labels'][LABEL_CONFIG_HASH] == \
  290. '689149e6041a85f6fb4945a2146a497ed43c8a5cbd8991753d875b165f1b4de4'
  291. assert opts['environment'] == ['also=real']
  292. def test_get_container_create_options_sets_affinity_with_binds(self):
  293. service = Service(
  294. 'foo',
  295. image='foo',
  296. client=self.mock_client,
  297. )
  298. self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
  299. prev_container = mock.Mock(
  300. id='ababab',
  301. image_config={'ContainerConfig': {'Volumes': ['/data']}})
  302. def container_get(key):
  303. return {
  304. 'Mounts': [
  305. {
  306. 'Destination': '/data',
  307. 'Source': '/some/path',
  308. 'Name': 'abab1234',
  309. },
  310. ]
  311. }.get(key, None)
  312. prev_container.get.side_effect = container_get
  313. prev_container.full_slug = 'abcdefff1234'
  314. opts = service._get_container_create_options(
  315. {},
  316. 1,
  317. previous_container=prev_container
  318. )
  319. assert opts['environment'] == ['affinity:container==ababab']
  320. def test_get_container_create_options_no_affinity_without_binds(self):
  321. service = Service('foo', image='foo', client=self.mock_client)
  322. self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
  323. prev_container = mock.Mock(
  324. id='ababab',
  325. image_config={'ContainerConfig': {}})
  326. prev_container.get.return_value = None
  327. prev_container.full_slug = 'abcdefff1234'
  328. opts = service._get_container_create_options(
  329. {},
  330. 1,
  331. previous_container=prev_container)
  332. assert opts['environment'] == []
  333. def test_get_container_not_found(self):
  334. self.mock_client.containers.return_value = []
  335. service = Service('foo', client=self.mock_client, image='foo')
  336. with pytest.raises(ValueError):
  337. service.get_container()
  338. @mock.patch('compose.service.Container', autospec=True)
  339. def test_get_container(self, mock_container_class):
  340. container_dict = dict(Name='default_foo_2_bdfa3ed91e2c')
  341. self.mock_client.containers.return_value = [container_dict]
  342. service = Service('foo', image='foo', client=self.mock_client)
  343. container = service.get_container(number=2)
  344. assert container == mock_container_class.from_ps.return_value
  345. mock_container_class.from_ps.assert_called_once_with(
  346. self.mock_client, container_dict)
  347. @mock.patch('compose.service.log', autospec=True)
  348. def test_pull_image(self, mock_log):
  349. service = Service('foo', client=self.mock_client, image='someimage:sometag')
  350. service.pull()
  351. self.mock_client.pull.assert_called_once_with(
  352. 'someimage',
  353. tag='sometag',
  354. stream=True,
  355. platform=None)
  356. mock_log.info.assert_called_once_with('Pulling foo (someimage:sometag)...')
  357. def test_pull_image_no_tag(self):
  358. service = Service('foo', client=self.mock_client, image='ababab')
  359. service.pull()
  360. self.mock_client.pull.assert_called_once_with(
  361. 'ababab',
  362. tag='latest',
  363. stream=True,
  364. platform=None)
  365. @mock.patch('compose.service.log', autospec=True)
  366. def test_pull_image_digest(self, mock_log):
  367. service = Service('foo', client=self.mock_client, image='someimage@sha256:1234')
  368. service.pull()
  369. self.mock_client.pull.assert_called_once_with(
  370. 'someimage',
  371. tag='sha256:1234',
  372. stream=True,
  373. platform=None)
  374. mock_log.info.assert_called_once_with('Pulling foo (someimage@sha256:1234)...')
  375. @mock.patch('compose.service.log', autospec=True)
  376. def test_pull_image_with_platform(self, mock_log):
  377. self.mock_client.api_version = '1.35'
  378. service = Service(
  379. 'foo', client=self.mock_client, image='someimage:sometag', platform='windows/x86_64'
  380. )
  381. service.pull()
  382. assert self.mock_client.pull.call_count == 1
  383. call_args = self.mock_client.pull.call_args
  384. assert call_args[1]['platform'] == 'windows/x86_64'
  385. @mock.patch('compose.service.log', autospec=True)
  386. def test_pull_image_with_platform_unsupported_api(self, mock_log):
  387. self.mock_client.api_version = '1.33'
  388. service = Service(
  389. 'foo', client=self.mock_client, image='someimage:sometag', platform='linux/arm'
  390. )
  391. with pytest.raises(OperationFailedError):
  392. service.pull()
  393. def test_pull_image_with_default_platform(self):
  394. self.mock_client.api_version = '1.35'
  395. service = Service(
  396. 'foo', client=self.mock_client, image='someimage:sometag',
  397. default_platform='linux'
  398. )
  399. assert service.platform == 'linux'
  400. service.pull()
  401. assert self.mock_client.pull.call_count == 1
  402. call_args = self.mock_client.pull.call_args
  403. assert call_args[1]['platform'] == 'linux'
  404. @mock.patch('compose.service.Container', autospec=True)
  405. def test_recreate_container(self, _):
  406. mock_container = mock.create_autospec(Container)
  407. mock_container.full_slug = 'abcdefff1234'
  408. service = Service('foo', client=self.mock_client, image='someimage')
  409. service.image = lambda: {'Id': 'abc123'}
  410. new_container = service.recreate_container(mock_container)
  411. mock_container.stop.assert_called_once_with(timeout=10)
  412. mock_container.rename_to_tmp_name.assert_called_once_with()
  413. new_container.start.assert_called_once_with()
  414. mock_container.remove.assert_called_once_with()
  415. @mock.patch('compose.service.Container', autospec=True)
  416. def test_recreate_container_with_timeout(self, _):
  417. mock_container = mock.create_autospec(Container)
  418. mock_container.full_slug = 'abcdefff1234'
  419. self.mock_client.inspect_image.return_value = {'Id': 'abc123'}
  420. service = Service('foo', client=self.mock_client, image='someimage')
  421. service.recreate_container(mock_container, timeout=1)
  422. mock_container.stop.assert_called_once_with(timeout=1)
  423. def test_parse_repository_tag(self):
  424. assert parse_repository_tag("root") == ("root", "", ":")
  425. assert parse_repository_tag("root:tag") == ("root", "tag", ":")
  426. assert parse_repository_tag("user/repo") == ("user/repo", "", ":")
  427. assert parse_repository_tag("user/repo:tag") == ("user/repo", "tag", ":")
  428. assert parse_repository_tag("url:5000/repo") == ("url:5000/repo", "", ":")
  429. assert parse_repository_tag("url:5000/repo:tag") == ("url:5000/repo", "tag", ":")
  430. assert parse_repository_tag("root@sha256:digest") == ("root", "sha256:digest", "@")
  431. assert parse_repository_tag("user/repo@sha256:digest") == ("user/repo", "sha256:digest", "@")
  432. assert parse_repository_tag("url:5000/repo@sha256:digest") == (
  433. "url:5000/repo", "sha256:digest", "@"
  434. )
  435. def test_create_container(self):
  436. service = Service('foo', client=self.mock_client, build={'context': '.'})
  437. self.mock_client.inspect_image.side_effect = [
  438. NoSuchImageError,
  439. {'Id': 'abc123'},
  440. ]
  441. self.mock_client.build.return_value = [
  442. '{"stream": "Successfully built abcd"}',
  443. ]
  444. with mock.patch('compose.service.log', autospec=True) as mock_log:
  445. service.create_container()
  446. assert mock_log.warning.called
  447. _, args, _ = mock_log.warning.mock_calls[0]
  448. assert 'was built because it did not already exist' in args[0]
  449. assert self.mock_client.build.call_count == 1
  450. assert self.mock_client.build.call_args[1]['tag'] == 'default_foo'
  451. def test_create_container_binary_string_error(self):
  452. service = Service('foo', client=self.mock_client, build={'context': '.'})
  453. service.image = lambda: {'Id': 'abc123'}
  454. self.mock_client.create_container.side_effect = APIError(None,
  455. None,
  456. b"Test binary string explanation")
  457. with pytest.raises(OperationFailedError) as ex:
  458. service.create_container()
  459. assert ex.value.msg == "Cannot create container for service foo: Test binary string explanation"
  460. def test_start_binary_string_error(self):
  461. service = Service('foo', client=self.mock_client)
  462. container = Container(self.mock_client, {'Id': 'abc123'})
  463. self.mock_client.start.side_effect = APIError(None,
  464. None,
  465. b"Test binary string explanation with "
  466. b"driver failed programming external "
  467. b"connectivity")
  468. with mock.patch('compose.service.log', autospec=True) as mock_log:
  469. with pytest.raises(OperationFailedError) as ex:
  470. service.start_container(container)
  471. assert ex.value.msg == "Cannot start service foo: " \
  472. "Test binary string explanation " \
  473. "with driver failed programming external connectivity"
  474. mock_log.warn.assert_called_once_with("Host is already in use by another container")
  475. def test_ensure_image_exists_no_build(self):
  476. service = Service('foo', client=self.mock_client, build={'context': '.'})
  477. self.mock_client.inspect_image.return_value = {'Id': 'abc123'}
  478. service.ensure_image_exists(do_build=BuildAction.skip)
  479. assert not self.mock_client.build.called
  480. def test_ensure_image_exists_no_build_but_needs_build(self):
  481. service = Service('foo', client=self.mock_client, build={'context': '.'})
  482. self.mock_client.inspect_image.side_effect = NoSuchImageError
  483. with pytest.raises(NeedsBuildError):
  484. service.ensure_image_exists(do_build=BuildAction.skip)
  485. def test_ensure_image_exists_force_build(self):
  486. service = Service('foo', client=self.mock_client, build={'context': '.'})
  487. self.mock_client.inspect_image.return_value = {'Id': 'abc123'}
  488. self.mock_client.build.return_value = [
  489. '{"stream": "Successfully built abcd"}',
  490. ]
  491. with mock.patch('compose.service.log', autospec=True) as mock_log:
  492. service.ensure_image_exists(do_build=BuildAction.force)
  493. assert not mock_log.warning.called
  494. assert self.mock_client.build.call_count == 1
  495. self.mock_client.build.call_args[1]['tag'] == 'default_foo'
  496. def test_build_does_not_pull(self):
  497. self.mock_client.build.return_value = [
  498. b'{"stream": "Successfully built 12345"}',
  499. ]
  500. service = Service('foo', client=self.mock_client, build={'context': '.'})
  501. service.build()
  502. assert self.mock_client.build.call_count == 1
  503. assert not self.mock_client.build.call_args[1]['pull']
  504. def test_build_with_platform(self):
  505. self.mock_client.api_version = '1.35'
  506. self.mock_client.build.return_value = [
  507. b'{"stream": "Successfully built 12345"}',
  508. ]
  509. service = Service('foo', client=self.mock_client, build={'context': '.'}, platform='linux')
  510. service.build()
  511. assert self.mock_client.build.call_count == 1
  512. call_args = self.mock_client.build.call_args
  513. assert call_args[1]['platform'] == 'linux'
  514. def test_build_with_default_platform(self):
  515. self.mock_client.api_version = '1.35'
  516. self.mock_client.build.return_value = [
  517. b'{"stream": "Successfully built 12345"}',
  518. ]
  519. service = Service(
  520. 'foo', client=self.mock_client, build={'context': '.'},
  521. default_platform='linux'
  522. )
  523. assert service.platform == 'linux'
  524. service.build()
  525. assert self.mock_client.build.call_count == 1
  526. call_args = self.mock_client.build.call_args
  527. assert call_args[1]['platform'] == 'linux'
  528. def test_service_platform_precedence(self):
  529. self.mock_client.api_version = '1.35'
  530. service = Service(
  531. 'foo', client=self.mock_client, platform='linux/arm',
  532. default_platform='osx'
  533. )
  534. assert service.platform == 'linux/arm'
  535. def test_service_ignore_default_platform_with_unsupported_api(self):
  536. self.mock_client.api_version = '1.32'
  537. self.mock_client.build.return_value = [
  538. b'{"stream": "Successfully built 12345"}',
  539. ]
  540. service = Service(
  541. 'foo', client=self.mock_client, default_platform='windows', build={'context': '.'}
  542. )
  543. assert service.platform is None
  544. service.build()
  545. assert self.mock_client.build.call_count == 1
  546. call_args = self.mock_client.build.call_args
  547. assert call_args[1]['platform'] is None
  548. def test_build_with_override_build_args(self):
  549. self.mock_client.build.return_value = [
  550. b'{"stream": "Successfully built 12345"}',
  551. ]
  552. build_args = {
  553. 'arg1': 'arg1_new_value',
  554. }
  555. service = Service('foo', client=self.mock_client,
  556. build={'context': '.', 'args': {'arg1': 'arg1', 'arg2': 'arg2'}})
  557. service.build(build_args_override=build_args)
  558. called_build_args = self.mock_client.build.call_args[1]['buildargs']
  559. assert called_build_args['arg1'] == build_args['arg1']
  560. assert called_build_args['arg2'] == 'arg2'
  561. def test_build_with_isolation_from_service_config(self):
  562. self.mock_client.build.return_value = [
  563. b'{"stream": "Successfully built 12345"}',
  564. ]
  565. service = Service('foo', client=self.mock_client, build={'context': '.'}, isolation='hyperv')
  566. service.build()
  567. assert self.mock_client.build.call_count == 1
  568. called_build_args = self.mock_client.build.call_args[1]
  569. assert called_build_args['isolation'] == 'hyperv'
  570. def test_build_isolation_from_build_override_service_config(self):
  571. self.mock_client.build.return_value = [
  572. b'{"stream": "Successfully built 12345"}',
  573. ]
  574. service = Service(
  575. 'foo', client=self.mock_client, build={'context': '.', 'isolation': 'default'},
  576. isolation='hyperv'
  577. )
  578. service.build()
  579. assert self.mock_client.build.call_count == 1
  580. called_build_args = self.mock_client.build.call_args[1]
  581. assert called_build_args['isolation'] == 'default'
  582. def test_config_dict(self):
  583. self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
  584. service = Service(
  585. 'foo',
  586. image='example.com/foo',
  587. client=self.mock_client,
  588. network_mode=ServiceNetworkMode(Service('other')),
  589. networks={'default': None},
  590. links=[(Service('one'), 'one')],
  591. volumes_from=[VolumeFromSpec(Service('two'), 'rw', 'service')])
  592. config_dict = service.config_dict()
  593. expected = {
  594. 'image_id': 'abcd',
  595. 'options': {'image': 'example.com/foo'},
  596. 'links': [('one', 'one')],
  597. 'net': 'other',
  598. 'secrets': [],
  599. 'networks': {'default': None},
  600. 'volumes_from': [('two', 'rw')],
  601. }
  602. assert config_dict == expected
  603. def test_config_dict_with_network_mode_from_container(self):
  604. self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
  605. container = Container(
  606. self.mock_client,
  607. {'Id': 'aaabbb', 'Name': '/foo_1'})
  608. service = Service(
  609. 'foo',
  610. image='example.com/foo',
  611. client=self.mock_client,
  612. network_mode=ContainerNetworkMode(container))
  613. config_dict = service.config_dict()
  614. expected = {
  615. 'image_id': 'abcd',
  616. 'options': {'image': 'example.com/foo'},
  617. 'links': [],
  618. 'networks': {},
  619. 'secrets': [],
  620. 'net': 'aaabbb',
  621. 'volumes_from': [],
  622. }
  623. assert config_dict == expected
  624. def test_config_hash_matches_label(self):
  625. self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
  626. service = Service(
  627. 'foo',
  628. image='example.com/foo',
  629. client=self.mock_client,
  630. network_mode=NetworkMode('bridge'),
  631. networks={'bridge': {}, 'net2': {}},
  632. links=[(Service('one', client=self.mock_client), 'one')],
  633. volumes_from=[VolumeFromSpec(Service('two', client=self.mock_client), 'rw', 'service')],
  634. volumes=[VolumeSpec('/ext', '/int', 'ro')],
  635. build={'context': 'some/random/path'},
  636. )
  637. config_hash = service.config_hash
  638. for api_version in set(API_VERSIONS.values()):
  639. self.mock_client.api_version = api_version
  640. assert service._get_container_create_options(
  641. {}, 1
  642. )['labels'][LABEL_CONFIG_HASH] == config_hash
  643. def test_remove_image_none(self):
  644. web = Service('web', image='example', client=self.mock_client)
  645. assert not web.remove_image(ImageType.none)
  646. assert not self.mock_client.remove_image.called
  647. def test_remove_image_local_with_image_name_doesnt_remove(self):
  648. web = Service('web', image='example', client=self.mock_client)
  649. assert not web.remove_image(ImageType.local)
  650. assert not self.mock_client.remove_image.called
  651. def test_remove_image_local_without_image_name_does_remove(self):
  652. web = Service('web', build='.', client=self.mock_client)
  653. assert web.remove_image(ImageType.local)
  654. self.mock_client.remove_image.assert_called_once_with(web.image_name)
  655. def test_remove_image_all_does_remove(self):
  656. web = Service('web', image='example', client=self.mock_client)
  657. assert web.remove_image(ImageType.all)
  658. self.mock_client.remove_image.assert_called_once_with(web.image_name)
  659. def test_remove_image_with_error(self):
  660. self.mock_client.remove_image.side_effect = error = APIError(
  661. message="testing",
  662. response={},
  663. explanation="Boom")
  664. web = Service('web', image='example', client=self.mock_client)
  665. with mock.patch('compose.service.log', autospec=True) as mock_log:
  666. assert not web.remove_image(ImageType.all)
  667. mock_log.error.assert_called_once_with(
  668. "Failed to remove image for service %s: %s", web.name, error)
  669. def test_remove_non_existing_image(self):
  670. self.mock_client.remove_image.side_effect = ImageNotFound('image not found')
  671. web = Service('web', image='example', client=self.mock_client)
  672. with mock.patch('compose.service.log', autospec=True) as mock_log:
  673. assert not web.remove_image(ImageType.all)
  674. mock_log.warning.assert_called_once_with("Image %s not found.", web.image_name)
  675. def test_specifies_host_port_with_no_ports(self):
  676. service = Service(
  677. 'foo',
  678. image='foo')
  679. assert not service.specifies_host_port()
  680. def test_specifies_host_port_with_container_port(self):
  681. service = Service(
  682. 'foo',
  683. image='foo',
  684. ports=["2000"])
  685. assert not service.specifies_host_port()
  686. def test_specifies_host_port_with_host_port(self):
  687. service = Service(
  688. 'foo',
  689. image='foo',
  690. ports=["1000:2000"])
  691. assert service.specifies_host_port()
  692. def test_specifies_host_port_with_host_ip_no_port(self):
  693. service = Service(
  694. 'foo',
  695. image='foo',
  696. ports=["127.0.0.1::2000"])
  697. assert not service.specifies_host_port()
  698. def test_specifies_host_port_with_host_ip_and_port(self):
  699. service = Service(
  700. 'foo',
  701. image='foo',
  702. ports=["127.0.0.1:1000:2000"])
  703. assert service.specifies_host_port()
  704. def test_specifies_host_port_with_container_port_range(self):
  705. service = Service(
  706. 'foo',
  707. image='foo',
  708. ports=["2000-3000"])
  709. assert not service.specifies_host_port()
  710. def test_specifies_host_port_with_host_port_range(self):
  711. service = Service(
  712. 'foo',
  713. image='foo',
  714. ports=["1000-2000:2000-3000"])
  715. assert service.specifies_host_port()
  716. def test_specifies_host_port_with_host_ip_no_port_range(self):
  717. service = Service(
  718. 'foo',
  719. image='foo',
  720. ports=["127.0.0.1::2000-3000"])
  721. assert not service.specifies_host_port()
  722. def test_specifies_host_port_with_host_ip_and_port_range(self):
  723. service = Service(
  724. 'foo',
  725. image='foo',
  726. ports=["127.0.0.1:1000-2000:2000-3000"])
  727. assert service.specifies_host_port()
  728. def test_image_name_from_config(self):
  729. image_name = 'example/web:mytag'
  730. service = Service('foo', image=image_name)
  731. assert service.image_name == image_name
  732. def test_image_name_default(self):
  733. service = Service('foo', project='testing')
  734. assert service.image_name == 'testing_foo'
  735. @mock.patch('compose.service.log', autospec=True)
  736. def test_only_log_warning_when_host_ports_clash(self, mock_log):
  737. self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
  738. ParallelStreamWriter.instance = None
  739. name = 'foo'
  740. service = Service(
  741. name,
  742. client=self.mock_client,
  743. ports=["8080:80"])
  744. service.scale(0)
  745. assert not mock_log.warning.called
  746. service.scale(1)
  747. assert not mock_log.warning.called
  748. service.scale(2)
  749. mock_log.warning.assert_called_once_with(
  750. 'The "{}" service specifies a port on the host. If multiple containers '
  751. 'for this service are created on a single host, the port will clash.'.format(name))
  752. def test_parse_proxy_config(self):
  753. default_proxy_config = {
  754. 'httpProxy': 'http://proxy.mycorp.com:3128',
  755. 'httpsProxy': 'https://user:[email protected]:3129',
  756. 'ftpProxy': 'http://ftpproxy.mycorp.com:21',
  757. 'noProxy': '*.intra.mycorp.com',
  758. }
  759. self.mock_client.base_url = 'http+docker://localunixsocket'
  760. self.mock_client._general_configs = {
  761. 'proxies': {
  762. 'default': default_proxy_config,
  763. }
  764. }
  765. service = Service('foo', client=self.mock_client)
  766. assert service._parse_proxy_config() == {
  767. 'HTTP_PROXY': default_proxy_config['httpProxy'],
  768. 'http_proxy': default_proxy_config['httpProxy'],
  769. 'HTTPS_PROXY': default_proxy_config['httpsProxy'],
  770. 'https_proxy': default_proxy_config['httpsProxy'],
  771. 'FTP_PROXY': default_proxy_config['ftpProxy'],
  772. 'ftp_proxy': default_proxy_config['ftpProxy'],
  773. 'NO_PROXY': default_proxy_config['noProxy'],
  774. 'no_proxy': default_proxy_config['noProxy'],
  775. }
  776. def test_parse_proxy_config_per_host(self):
  777. default_proxy_config = {
  778. 'httpProxy': 'http://proxy.mycorp.com:3128',
  779. 'httpsProxy': 'https://user:[email protected]:3129',
  780. 'ftpProxy': 'http://ftpproxy.mycorp.com:21',
  781. 'noProxy': '*.intra.mycorp.com',
  782. }
  783. host_specific_proxy_config = {
  784. 'httpProxy': 'http://proxy.example.com:3128',
  785. 'httpsProxy': 'https://user:[email protected]:3129',
  786. 'ftpProxy': 'http://ftpproxy.example.com:21',
  787. 'noProxy': '*.intra.example.com'
  788. }
  789. self.mock_client.base_url = 'http+docker://localunixsocket'
  790. self.mock_client._general_configs = {
  791. 'proxies': {
  792. 'default': default_proxy_config,
  793. 'tcp://example.docker.com:2376': host_specific_proxy_config,
  794. }
  795. }
  796. service = Service('foo', client=self.mock_client)
  797. assert service._parse_proxy_config() == {
  798. 'HTTP_PROXY': default_proxy_config['httpProxy'],
  799. 'http_proxy': default_proxy_config['httpProxy'],
  800. 'HTTPS_PROXY': default_proxy_config['httpsProxy'],
  801. 'https_proxy': default_proxy_config['httpsProxy'],
  802. 'FTP_PROXY': default_proxy_config['ftpProxy'],
  803. 'ftp_proxy': default_proxy_config['ftpProxy'],
  804. 'NO_PROXY': default_proxy_config['noProxy'],
  805. 'no_proxy': default_proxy_config['noProxy'],
  806. }
  807. self.mock_client._original_base_url = 'tcp://example.docker.com:2376'
  808. assert service._parse_proxy_config() == {
  809. 'HTTP_PROXY': host_specific_proxy_config['httpProxy'],
  810. 'http_proxy': host_specific_proxy_config['httpProxy'],
  811. 'HTTPS_PROXY': host_specific_proxy_config['httpsProxy'],
  812. 'https_proxy': host_specific_proxy_config['httpsProxy'],
  813. 'FTP_PROXY': host_specific_proxy_config['ftpProxy'],
  814. 'ftp_proxy': host_specific_proxy_config['ftpProxy'],
  815. 'NO_PROXY': host_specific_proxy_config['noProxy'],
  816. 'no_proxy': host_specific_proxy_config['noProxy'],
  817. }
  818. def test_build_service_with_proxy_config(self):
  819. default_proxy_config = {
  820. 'httpProxy': 'http://proxy.mycorp.com:3128',
  821. 'httpsProxy': 'https://user:[email protected]:3129',
  822. }
  823. buildargs = {
  824. 'HTTPS_PROXY': 'https://rdcf.th08.jp:8911',
  825. 'https_proxy': 'https://rdcf.th08.jp:8911',
  826. }
  827. self.mock_client._general_configs = {
  828. 'proxies': {
  829. 'default': default_proxy_config,
  830. }
  831. }
  832. self.mock_client.base_url = 'http+docker://localunixsocket'
  833. self.mock_client.build.return_value = [
  834. b'{"stream": "Successfully built 12345"}',
  835. ]
  836. service = Service('foo', client=self.mock_client, build={'context': '.', 'args': buildargs})
  837. service.build()
  838. assert self.mock_client.build.call_count == 1
  839. assert self.mock_client.build.call_args[1]['buildargs'] == {
  840. 'HTTP_PROXY': default_proxy_config['httpProxy'],
  841. 'http_proxy': default_proxy_config['httpProxy'],
  842. 'HTTPS_PROXY': buildargs['HTTPS_PROXY'],
  843. 'https_proxy': buildargs['HTTPS_PROXY'],
  844. }
  845. def test_get_create_options_with_proxy_config(self):
  846. default_proxy_config = {
  847. 'httpProxy': 'http://proxy.mycorp.com:3128',
  848. 'httpsProxy': 'https://user:[email protected]:3129',
  849. 'ftpProxy': 'http://ftpproxy.mycorp.com:21',
  850. }
  851. self.mock_client._general_configs = {
  852. 'proxies': {
  853. 'default': default_proxy_config,
  854. }
  855. }
  856. self.mock_client.base_url = 'http+docker://localunixsocket'
  857. override_options = {
  858. 'environment': {
  859. 'FTP_PROXY': 'ftp://xdge.exo.au:21',
  860. 'ftp_proxy': 'ftp://xdge.exo.au:21',
  861. }
  862. }
  863. environment = {
  864. 'HTTPS_PROXY': 'https://rdcf.th08.jp:8911',
  865. 'https_proxy': 'https://rdcf.th08.jp:8911',
  866. }
  867. service = Service('foo', client=self.mock_client, environment=environment)
  868. create_opts = service._get_container_create_options(override_options, 1)
  869. assert set(create_opts['environment']) == set(format_environment({
  870. 'HTTP_PROXY': default_proxy_config['httpProxy'],
  871. 'http_proxy': default_proxy_config['httpProxy'],
  872. 'HTTPS_PROXY': environment['HTTPS_PROXY'],
  873. 'https_proxy': environment['HTTPS_PROXY'],
  874. 'FTP_PROXY': override_options['environment']['FTP_PROXY'],
  875. 'ftp_proxy': override_options['environment']['FTP_PROXY'],
  876. }))
  877. def test_create_when_removed_containers_are_listed(self):
  878. # This is aimed at simulating a race between the API call to list the
  879. # containers, and the ones to inspect each of the listed containers.
  880. # It can happen that a container has been removed after we listed it.
  881. # containers() returns a container that is about to be removed
  882. self.mock_client.containers.return_value = [
  883. {'Id': 'rm_cont_id', 'Name': 'rm_cont', 'Image': 'img_id'},
  884. ]
  885. # inspect_container() will raise a NotFound when trying to inspect
  886. # rm_cont_id, which at this point has been removed
  887. def inspect(name):
  888. if name == 'rm_cont_id':
  889. raise NotFound(message='Not Found')
  890. if name == 'new_cont_id':
  891. return {'Id': 'new_cont_id'}
  892. raise NotImplementedError("incomplete mock")
  893. self.mock_client.inspect_container.side_effect = inspect
  894. self.mock_client.inspect_image.return_value = {'Id': 'imageid'}
  895. self.mock_client.create_container.return_value = {'Id': 'new_cont_id'}
  896. # We should nonetheless be able to create a new container
  897. service = Service('foo', client=self.mock_client)
  898. assert service.create_container().id == 'new_cont_id'
  899. def test_build_volume_options_duplicate_binds(self):
  900. self.mock_client.api_version = '1.29' # Trigger 3.2 format workaround
  901. service = Service('foo', client=self.mock_client)
  902. ctnr_opts, override_opts = service._build_container_volume_options(
  903. previous_container=None,
  904. container_options={
  905. 'volumes': [
  906. MountSpec.parse({'source': 'vol', 'target': '/data', 'type': 'volume'}),
  907. VolumeSpec.parse('vol:/data:rw'),
  908. ],
  909. 'environment': {},
  910. },
  911. override_options={},
  912. )
  913. assert 'binds' in override_opts
  914. assert len(override_opts['binds']) == 1
  915. assert override_opts['binds'][0] == 'vol:/data:rw'
  916. def test_volumes_order_is_preserved(self):
  917. service = Service('foo', client=self.mock_client)
  918. volumes = [
  919. VolumeSpec.parse(cfg) for cfg in [
  920. '/v{0}:/v{0}:rw'.format(i) for i in range(6)
  921. ]
  922. ]
  923. ctnr_opts, override_opts = service._build_container_volume_options(
  924. previous_container=None,
  925. container_options={
  926. 'volumes': volumes,
  927. 'environment': {},
  928. },
  929. override_options={},
  930. )
  931. assert override_opts['binds'] == [vol.repr() for vol in volumes]
  932. class TestServiceNetwork(unittest.TestCase):
  933. def setUp(self):
  934. self.mock_client = mock.create_autospec(docker.APIClient)
  935. self.mock_client.api_version = DEFAULT_DOCKER_API_VERSION
  936. self.mock_client._general_configs = {}
  937. def test_connect_container_to_networks_short_aliase_exists(self):
  938. service = Service(
  939. 'db',
  940. self.mock_client,
  941. 'myproject',
  942. image='foo',
  943. networks={'project_default': {}})
  944. container = Container(
  945. None,
  946. {
  947. 'Id': 'abcdef',
  948. 'NetworkSettings': {
  949. 'Networks': {
  950. 'project_default': {
  951. 'Aliases': ['analias', 'abcdef'],
  952. },
  953. },
  954. },
  955. },
  956. True)
  957. service.connect_container_to_networks(container)
  958. assert not self.mock_client.disconnect_container_from_network.call_count
  959. assert not self.mock_client.connect_container_to_network.call_count
  960. def sort_by_name(dictionary_list):
  961. return sorted(dictionary_list, key=lambda k: k['name'])
  962. class BuildUlimitsTestCase(unittest.TestCase):
  963. def test_build_ulimits_with_dict(self):
  964. ulimits = build_ulimits(
  965. {
  966. 'nofile': {'soft': 10000, 'hard': 20000},
  967. 'nproc': {'soft': 65535, 'hard': 65535}
  968. }
  969. )
  970. expected = [
  971. {'name': 'nofile', 'soft': 10000, 'hard': 20000},
  972. {'name': 'nproc', 'soft': 65535, 'hard': 65535}
  973. ]
  974. assert sort_by_name(ulimits) == sort_by_name(expected)
  975. def test_build_ulimits_with_ints(self):
  976. ulimits = build_ulimits({'nofile': 20000, 'nproc': 65535})
  977. expected = [
  978. {'name': 'nofile', 'soft': 20000, 'hard': 20000},
  979. {'name': 'nproc', 'soft': 65535, 'hard': 65535}
  980. ]
  981. assert sort_by_name(ulimits) == sort_by_name(expected)
  982. def test_build_ulimits_with_integers_and_dicts(self):
  983. ulimits = build_ulimits(
  984. {
  985. 'nproc': 65535,
  986. 'nofile': {'soft': 10000, 'hard': 20000}
  987. }
  988. )
  989. expected = [
  990. {'name': 'nofile', 'soft': 10000, 'hard': 20000},
  991. {'name': 'nproc', 'soft': 65535, 'hard': 65535}
  992. ]
  993. assert sort_by_name(ulimits) == sort_by_name(expected)
  994. class NetTestCase(unittest.TestCase):
  995. def setUp(self):
  996. self.mock_client = mock.create_autospec(docker.APIClient)
  997. self.mock_client.api_version = DEFAULT_DOCKER_API_VERSION
  998. self.mock_client._general_configs = {}
  999. def test_network_mode(self):
  1000. network_mode = NetworkMode('host')
  1001. assert network_mode.id == 'host'
  1002. assert network_mode.mode == 'host'
  1003. assert network_mode.service_name is None
  1004. def test_network_mode_container(self):
  1005. container_id = 'abcd'
  1006. network_mode = ContainerNetworkMode(Container(None, {'Id': container_id}))
  1007. assert network_mode.id == container_id
  1008. assert network_mode.mode == 'container:' + container_id
  1009. assert network_mode.service_name is None
  1010. def test_network_mode_service(self):
  1011. container_id = 'bbbb'
  1012. service_name = 'web'
  1013. self.mock_client.containers.return_value = [
  1014. {'Id': container_id, 'Name': container_id, 'Image': 'abcd'},
  1015. ]
  1016. service = Service(name=service_name, client=self.mock_client)
  1017. network_mode = ServiceNetworkMode(service)
  1018. assert network_mode.id == service_name
  1019. assert network_mode.mode == 'container:' + container_id
  1020. assert network_mode.service_name == service_name
  1021. def test_network_mode_service_no_containers(self):
  1022. service_name = 'web'
  1023. self.mock_client.containers.return_value = []
  1024. service = Service(name=service_name, client=self.mock_client)
  1025. network_mode = ServiceNetworkMode(service)
  1026. assert network_mode.id == service_name
  1027. assert network_mode.mode is None
  1028. assert network_mode.service_name == service_name
  1029. class ServicePortsTest(unittest.TestCase):
  1030. def test_formatted_ports(self):
  1031. ports = [
  1032. '3000',
  1033. '0.0.0.0:4025-4030:23000-23005',
  1034. ServicePort(6000, None, None, None, None),
  1035. ServicePort(8080, 8080, None, None, None),
  1036. ServicePort('20000', '20000', 'udp', 'ingress', None),
  1037. ServicePort(30000, '30000', 'tcp', None, '127.0.0.1'),
  1038. ]
  1039. formatted = formatted_ports(ports)
  1040. assert ports[0] in formatted
  1041. assert ports[1] in formatted
  1042. assert '6000/tcp' in formatted
  1043. assert '8080:8080/tcp' in formatted
  1044. assert '20000:20000/udp' in formatted
  1045. assert '127.0.0.1:30000:30000/tcp' in formatted
  1046. def build_mount(destination, source, mode='rw'):
  1047. return {'Source': source, 'Destination': destination, 'Mode': mode}
  1048. class ServiceVolumesTest(unittest.TestCase):
  1049. def setUp(self):
  1050. self.mock_client = mock.create_autospec(docker.APIClient)
  1051. self.mock_client.api_version = DEFAULT_DOCKER_API_VERSION
  1052. self.mock_client._general_configs = {}
  1053. def test_build_volume_binding(self):
  1054. binding = build_volume_binding(VolumeSpec.parse('/outside:/inside', True))
  1055. assert binding == ('/inside', '/outside:/inside:rw')
  1056. def test_get_container_data_volumes(self):
  1057. options = [VolumeSpec.parse(v) for v in [
  1058. '/host/volume:/host/volume:ro',
  1059. '/new/volume',
  1060. '/existing/volume',
  1061. 'named:/named/vol',
  1062. '/dev/tmpfs'
  1063. ]]
  1064. self.mock_client.inspect_image.return_value = {
  1065. 'ContainerConfig': {
  1066. 'Volumes': {
  1067. '/mnt/image/data': {},
  1068. }
  1069. }
  1070. }
  1071. container = Container(self.mock_client, {
  1072. 'Image': 'ababab',
  1073. 'Mounts': [
  1074. {
  1075. 'Source': '/host/volume',
  1076. 'Destination': '/host/volume',
  1077. 'Mode': '',
  1078. 'RW': True,
  1079. 'Name': 'hostvolume',
  1080. }, {
  1081. 'Source': '/var/lib/docker/aaaaaaaa',
  1082. 'Destination': '/existing/volume',
  1083. 'Mode': '',
  1084. 'RW': True,
  1085. 'Name': 'existingvolume',
  1086. }, {
  1087. 'Source': '/var/lib/docker/bbbbbbbb',
  1088. 'Destination': '/removed/volume',
  1089. 'Mode': '',
  1090. 'RW': True,
  1091. 'Name': 'removedvolume',
  1092. }, {
  1093. 'Source': '/var/lib/docker/cccccccc',
  1094. 'Destination': '/mnt/image/data',
  1095. 'Mode': '',
  1096. 'RW': True,
  1097. 'Name': 'imagedata',
  1098. },
  1099. ]
  1100. }, has_been_inspected=True)
  1101. expected = [
  1102. VolumeSpec.parse('existingvolume:/existing/volume:rw'),
  1103. VolumeSpec.parse('imagedata:/mnt/image/data:rw'),
  1104. ]
  1105. volumes, _ = get_container_data_volumes(container, options, ['/dev/tmpfs'], [])
  1106. assert sorted(volumes) == sorted(expected)
  1107. def test_merge_volume_bindings(self):
  1108. options = [
  1109. VolumeSpec.parse(v, True) for v in [
  1110. '/host/volume:/host/volume:ro',
  1111. '/host/rw/volume:/host/rw/volume',
  1112. '/new/volume',
  1113. '/existing/volume',
  1114. '/dev/tmpfs'
  1115. ]
  1116. ]
  1117. self.mock_client.inspect_image.return_value = {
  1118. 'ContainerConfig': {'Volumes': {}}
  1119. }
  1120. previous_container = Container(self.mock_client, {
  1121. 'Id': 'cdefab',
  1122. 'Image': 'ababab',
  1123. 'Mounts': [{
  1124. 'Source': '/var/lib/docker/aaaaaaaa',
  1125. 'Destination': '/existing/volume',
  1126. 'Mode': '',
  1127. 'RW': True,
  1128. 'Name': 'existingvolume',
  1129. }],
  1130. }, has_been_inspected=True)
  1131. expected = [
  1132. '/host/volume:/host/volume:ro',
  1133. '/host/rw/volume:/host/rw/volume:rw',
  1134. 'existingvolume:/existing/volume:rw',
  1135. ]
  1136. binds, affinity = merge_volume_bindings(options, ['/dev/tmpfs'], previous_container, [])
  1137. assert sorted(binds) == sorted(expected)
  1138. assert affinity == {'affinity:container': '=cdefab'}
  1139. def test_mount_same_host_path_to_two_volumes(self):
  1140. service = Service(
  1141. 'web',
  1142. image='busybox',
  1143. volumes=[
  1144. VolumeSpec.parse('/host/path:/data1', True),
  1145. VolumeSpec.parse('/host/path:/data2', True),
  1146. ],
  1147. client=self.mock_client,
  1148. )
  1149. self.mock_client.inspect_image.return_value = {
  1150. 'Id': 'ababab',
  1151. 'ContainerConfig': {
  1152. 'Volumes': {}
  1153. }
  1154. }
  1155. service._get_container_create_options(
  1156. override_options={},
  1157. number=1,
  1158. )
  1159. assert set(self.mock_client.create_host_config.call_args[1]['binds']) == {'/host/path:/data1:rw',
  1160. '/host/path:/data2:rw'}
  1161. def test_get_container_create_options_with_different_host_path_in_container_json(self):
  1162. service = Service(
  1163. 'web',
  1164. image='busybox',
  1165. volumes=[VolumeSpec.parse('/host/path:/data')],
  1166. client=self.mock_client,
  1167. )
  1168. volume_name = 'abcdefff1234'
  1169. self.mock_client.inspect_image.return_value = {
  1170. 'Id': 'ababab',
  1171. 'ContainerConfig': {
  1172. 'Volumes': {
  1173. '/data': {},
  1174. }
  1175. }
  1176. }
  1177. self.mock_client.inspect_container.return_value = {
  1178. 'Id': '123123123',
  1179. 'Image': 'ababab',
  1180. 'Mounts': [
  1181. {
  1182. 'Destination': '/data',
  1183. 'Source': '/mnt/sda1/host/path',
  1184. 'Mode': '',
  1185. 'RW': True,
  1186. 'Driver': 'local',
  1187. 'Name': volume_name,
  1188. },
  1189. ]
  1190. }
  1191. service._get_container_create_options(
  1192. override_options={},
  1193. number=1,
  1194. previous_container=Container(self.mock_client, {'Id': '123123123'}),
  1195. )
  1196. assert (
  1197. self.mock_client.create_host_config.call_args[1]['binds'] ==
  1198. ['{}:/data:rw'.format(volume_name)]
  1199. )
  1200. def test_warn_on_masked_volume_no_warning_when_no_container_volumes(self):
  1201. volumes_option = [VolumeSpec('/home/user', '/path', 'rw')]
  1202. container_volumes = []
  1203. service = 'service_name'
  1204. with mock.patch('compose.service.log', autospec=True) as mock_log:
  1205. warn_on_masked_volume(volumes_option, container_volumes, service)
  1206. assert not mock_log.warning.called
  1207. def test_warn_on_masked_volume_when_masked(self):
  1208. volumes_option = [VolumeSpec('/home/user', '/path', 'rw')]
  1209. container_volumes = [
  1210. VolumeSpec('/var/lib/docker/path', '/path', 'rw'),
  1211. VolumeSpec('/var/lib/docker/path', '/other', 'rw'),
  1212. ]
  1213. service = 'service_name'
  1214. with mock.patch('compose.service.log', autospec=True) as mock_log:
  1215. warn_on_masked_volume(volumes_option, container_volumes, service)
  1216. mock_log.warning.assert_called_once_with(mock.ANY)
  1217. def test_warn_on_masked_no_warning_with_same_path(self):
  1218. volumes_option = [VolumeSpec('/home/user', '/path', 'rw')]
  1219. container_volumes = [VolumeSpec('/home/user', '/path', 'rw')]
  1220. service = 'service_name'
  1221. with mock.patch('compose.service.log', autospec=True) as mock_log:
  1222. warn_on_masked_volume(volumes_option, container_volumes, service)
  1223. assert not mock_log.warning.called
  1224. def test_warn_on_masked_no_warning_with_container_only_option(self):
  1225. volumes_option = [VolumeSpec(None, '/path', 'rw')]
  1226. container_volumes = [
  1227. VolumeSpec('/var/lib/docker/volume/path', '/path', 'rw')
  1228. ]
  1229. service = 'service_name'
  1230. with mock.patch('compose.service.log', autospec=True) as mock_log:
  1231. warn_on_masked_volume(volumes_option, container_volumes, service)
  1232. assert not mock_log.warning.called
  1233. def test_create_with_special_volume_mode(self):
  1234. self.mock_client.inspect_image.return_value = {'Id': 'imageid'}
  1235. self.mock_client.create_container.return_value = {'Id': 'containerid'}
  1236. volume = '/tmp:/foo:z'
  1237. Service(
  1238. 'web',
  1239. client=self.mock_client,
  1240. image='busybox',
  1241. volumes=[VolumeSpec.parse(volume, True)],
  1242. ).create_container()
  1243. assert self.mock_client.create_container.call_count == 1
  1244. assert self.mock_client.create_host_config.call_args[1]['binds'] == [volume]
  1245. class ServiceSecretTest(unittest.TestCase):
  1246. def setUp(self):
  1247. self.mock_client = mock.create_autospec(docker.APIClient)
  1248. self.mock_client.api_version = DEFAULT_DOCKER_API_VERSION
  1249. self.mock_client._general_configs = {}
  1250. def test_get_secret_volumes(self):
  1251. secret1 = {
  1252. 'secret': ServiceSecret.parse({'source': 'secret1', 'target': 'b.txt'}),
  1253. 'file': 'a.txt'
  1254. }
  1255. service = Service(
  1256. 'web',
  1257. client=self.mock_client,
  1258. image='busybox',
  1259. secrets=[secret1]
  1260. )
  1261. volumes = service.get_secret_volumes()
  1262. assert volumes[0].source == secret1['file']
  1263. assert volumes[0].target == '{}/{}'.format(SECRETS_PATH, secret1['secret'].target)
  1264. def test_get_secret_volumes_abspath(self):
  1265. secret1 = {
  1266. 'secret': ServiceSecret.parse({'source': 'secret1', 'target': '/d.txt'}),
  1267. 'file': 'c.txt'
  1268. }
  1269. service = Service(
  1270. 'web',
  1271. client=self.mock_client,
  1272. image='busybox',
  1273. secrets=[secret1]
  1274. )
  1275. volumes = service.get_secret_volumes()
  1276. assert volumes[0].source == secret1['file']
  1277. assert volumes[0].target == secret1['secret'].target
  1278. def test_get_secret_volumes_no_target(self):
  1279. secret1 = {
  1280. 'secret': ServiceSecret.parse({'source': 'secret1'}),
  1281. 'file': 'c.txt'
  1282. }
  1283. service = Service(
  1284. 'web',
  1285. client=self.mock_client,
  1286. image='busybox',
  1287. secrets=[secret1]
  1288. )
  1289. volumes = service.get_secret_volumes()
  1290. assert volumes[0].source == secret1['file']
  1291. assert volumes[0].target == '{}/{}'.format(SECRETS_PATH, secret1['secret'].source)
  1292. class RewriteBuildPathTest(unittest.TestCase):
  1293. @mock.patch('compose.service.IS_WINDOWS_PLATFORM', True)
  1294. def test_rewrite_url_no_prefix(self):
  1295. urls = [
  1296. 'http://test.com',
  1297. 'https://test.com',
  1298. 'git://test.com',
  1299. 'github.com/test/test',
  1300. '[email protected]',
  1301. ]
  1302. for u in urls:
  1303. assert rewrite_build_path(u) == u
  1304. @mock.patch('compose.service.IS_WINDOWS_PLATFORM', True)
  1305. def test_rewrite_windows_path(self):
  1306. assert rewrite_build_path('C:\\context') == WINDOWS_LONGPATH_PREFIX + 'C:\\context'
  1307. assert rewrite_build_path(
  1308. rewrite_build_path('C:\\context')
  1309. ) == rewrite_build_path('C:\\context')
  1310. @mock.patch('compose.service.IS_WINDOWS_PLATFORM', False)
  1311. def test_rewrite_unix_path(self):
  1312. assert rewrite_build_path('/context') == '/context'