1
0

service_test.py 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357
  1. from __future__ import absolute_import
  2. from __future__ import unicode_literals
  3. import os
  4. import shutil
  5. import tempfile
  6. from distutils.spawn import find_executable
  7. from os import path
  8. import pytest
  9. from docker.errors import APIError
  10. from six import StringIO
  11. from six import text_type
  12. from .. import mock
  13. from .testcases import DockerClientTestCase
  14. from .testcases import get_links
  15. from .testcases import pull_busybox
  16. from .testcases import SWARM_SKIP_CONTAINERS_ALL
  17. from .testcases import SWARM_SKIP_CPU_SHARES
  18. from compose import __version__
  19. from compose.config.types import VolumeFromSpec
  20. from compose.config.types import VolumeSpec
  21. from compose.const import IS_WINDOWS_PLATFORM
  22. from compose.const import LABEL_CONFIG_HASH
  23. from compose.const import LABEL_CONTAINER_NUMBER
  24. from compose.const import LABEL_ONE_OFF
  25. from compose.const import LABEL_PROJECT
  26. from compose.const import LABEL_SERVICE
  27. from compose.const import LABEL_VERSION
  28. from compose.container import Container
  29. from compose.errors import OperationFailedError
  30. from compose.project import OneOffFilter
  31. from compose.service import ConvergencePlan
  32. from compose.service import ConvergenceStrategy
  33. from compose.service import NetworkMode
  34. from compose.service import PidMode
  35. from compose.service import Service
  36. from tests.integration.testcases import is_cluster
  37. from tests.integration.testcases import no_cluster
  38. from tests.integration.testcases import v2_1_only
  39. from tests.integration.testcases import v2_2_only
  40. from tests.integration.testcases import v2_3_only
  41. from tests.integration.testcases import v2_only
  42. from tests.integration.testcases import v3_only
  43. def create_and_start_container(service, **override_options):
  44. container = service.create_container(**override_options)
  45. return service.start_container(container)
  46. class ServiceTest(DockerClientTestCase):
  47. def test_containers(self):
  48. foo = self.create_service('foo')
  49. bar = self.create_service('bar')
  50. create_and_start_container(foo)
  51. self.assertEqual(len(foo.containers()), 1)
  52. self.assertEqual(foo.containers()[0].name, 'composetest_foo_1')
  53. self.assertEqual(len(bar.containers()), 0)
  54. create_and_start_container(bar)
  55. create_and_start_container(bar)
  56. self.assertEqual(len(foo.containers()), 1)
  57. self.assertEqual(len(bar.containers()), 2)
  58. names = [c.name for c in bar.containers()]
  59. self.assertIn('composetest_bar_1', names)
  60. self.assertIn('composetest_bar_2', names)
  61. def test_containers_one_off(self):
  62. db = self.create_service('db')
  63. container = db.create_container(one_off=True)
  64. self.assertEqual(db.containers(stopped=True), [])
  65. self.assertEqual(db.containers(one_off=OneOffFilter.only, stopped=True), [container])
  66. def test_project_is_added_to_container_name(self):
  67. service = self.create_service('web')
  68. create_and_start_container(service)
  69. self.assertEqual(service.containers()[0].name, 'composetest_web_1')
  70. def test_create_container_with_one_off(self):
  71. db = self.create_service('db')
  72. container = db.create_container(one_off=True)
  73. self.assertEqual(container.name, 'composetest_db_run_1')
  74. def test_create_container_with_one_off_when_existing_container_is_running(self):
  75. db = self.create_service('db')
  76. db.start()
  77. container = db.create_container(one_off=True)
  78. self.assertEqual(container.name, 'composetest_db_run_1')
  79. def test_create_container_with_unspecified_volume(self):
  80. service = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])
  81. container = service.create_container()
  82. service.start_container(container)
  83. assert container.get_mount('/var/db')
  84. def test_create_container_with_volume_driver(self):
  85. service = self.create_service('db', volume_driver='foodriver')
  86. container = service.create_container()
  87. service.start_container(container)
  88. self.assertEqual('foodriver', container.get('HostConfig.VolumeDriver'))
  89. @pytest.mark.skipif(SWARM_SKIP_CPU_SHARES, reason='Swarm --cpu-shares bug')
  90. def test_create_container_with_cpu_shares(self):
  91. service = self.create_service('db', cpu_shares=73)
  92. container = service.create_container()
  93. service.start_container(container)
  94. self.assertEqual(container.get('HostConfig.CpuShares'), 73)
  95. def test_create_container_with_cpu_quota(self):
  96. service = self.create_service('db', cpu_quota=40000)
  97. container = service.create_container()
  98. container.start()
  99. self.assertEqual(container.get('HostConfig.CpuQuota'), 40000)
  100. @v2_2_only()
  101. def test_create_container_with_cpu_count(self):
  102. self.require_api_version('1.25')
  103. service = self.create_service('db', cpu_count=2)
  104. container = service.create_container()
  105. service.start_container(container)
  106. self.assertEqual(container.get('HostConfig.CpuCount'), 2)
  107. @v2_2_only()
  108. @pytest.mark.skipif(not IS_WINDOWS_PLATFORM, reason='cpu_percent is not supported for Linux')
  109. def test_create_container_with_cpu_percent(self):
  110. self.require_api_version('1.25')
  111. service = self.create_service('db', cpu_percent=12)
  112. container = service.create_container()
  113. service.start_container(container)
  114. self.assertEqual(container.get('HostConfig.CpuPercent'), 12)
  115. @v2_2_only()
  116. def test_create_container_with_cpus(self):
  117. self.require_api_version('1.25')
  118. service = self.create_service('db', cpus=1)
  119. container = service.create_container()
  120. service.start_container(container)
  121. self.assertEqual(container.get('HostConfig.NanoCpus'), 1000000000)
  122. def test_create_container_with_shm_size(self):
  123. self.require_api_version('1.22')
  124. service = self.create_service('db', shm_size=67108864)
  125. container = service.create_container()
  126. service.start_container(container)
  127. self.assertEqual(container.get('HostConfig.ShmSize'), 67108864)
  128. def test_create_container_with_init_bool(self):
  129. self.require_api_version('1.25')
  130. service = self.create_service('db', init=True)
  131. container = service.create_container()
  132. service.start_container(container)
  133. assert container.get('HostConfig.Init') is True
  134. @pytest.mark.xfail(True, reason='Option has been removed in Engine 17.06.0')
  135. def test_create_container_with_init_path(self):
  136. self.require_api_version('1.25')
  137. docker_init_path = find_executable('docker-init')
  138. service = self.create_service('db', init=docker_init_path)
  139. container = service.create_container()
  140. service.start_container(container)
  141. assert container.get('HostConfig.InitPath') == docker_init_path
  142. @pytest.mark.xfail(True, reason='Some kernels/configs do not support pids_limit')
  143. def test_create_container_with_pids_limit(self):
  144. self.require_api_version('1.23')
  145. service = self.create_service('db', pids_limit=10)
  146. container = service.create_container()
  147. service.start_container(container)
  148. assert container.get('HostConfig.PidsLimit') == 10
  149. def test_create_container_with_extra_hosts_list(self):
  150. extra_hosts = ['somehost:162.242.195.82', 'otherhost:50.31.209.229']
  151. service = self.create_service('db', extra_hosts=extra_hosts)
  152. container = service.create_container()
  153. service.start_container(container)
  154. self.assertEqual(set(container.get('HostConfig.ExtraHosts')), set(extra_hosts))
  155. def test_create_container_with_extra_hosts_dicts(self):
  156. extra_hosts = {'somehost': '162.242.195.82', 'otherhost': '50.31.209.229'}
  157. extra_hosts_list = ['somehost:162.242.195.82', 'otherhost:50.31.209.229']
  158. service = self.create_service('db', extra_hosts=extra_hosts)
  159. container = service.create_container()
  160. service.start_container(container)
  161. self.assertEqual(set(container.get('HostConfig.ExtraHosts')), set(extra_hosts_list))
  162. def test_create_container_with_cpu_set(self):
  163. service = self.create_service('db', cpuset='0')
  164. container = service.create_container()
  165. service.start_container(container)
  166. self.assertEqual(container.get('HostConfig.CpusetCpus'), '0')
  167. def test_create_container_with_read_only_root_fs(self):
  168. read_only = True
  169. service = self.create_service('db', read_only=read_only)
  170. container = service.create_container()
  171. service.start_container(container)
  172. assert container.get('HostConfig.ReadonlyRootfs') == read_only
  173. def test_create_container_with_blkio_config(self):
  174. blkio_config = {
  175. 'weight': 300,
  176. 'weight_device': [{'path': '/dev/sda', 'weight': 200}],
  177. 'device_read_bps': [{'path': '/dev/sda', 'rate': 1024 * 1024 * 100}],
  178. 'device_read_iops': [{'path': '/dev/sda', 'rate': 1000}],
  179. 'device_write_bps': [{'path': '/dev/sda', 'rate': 1024 * 1024}],
  180. 'device_write_iops': [{'path': '/dev/sda', 'rate': 800}]
  181. }
  182. service = self.create_service('web', blkio_config=blkio_config)
  183. container = service.create_container()
  184. assert container.get('HostConfig.BlkioWeight') == 300
  185. assert container.get('HostConfig.BlkioWeightDevice') == [{
  186. 'Path': '/dev/sda', 'Weight': 200
  187. }]
  188. assert container.get('HostConfig.BlkioDeviceReadBps') == [{
  189. 'Path': '/dev/sda', 'Rate': 1024 * 1024 * 100
  190. }]
  191. assert container.get('HostConfig.BlkioDeviceWriteBps') == [{
  192. 'Path': '/dev/sda', 'Rate': 1024 * 1024
  193. }]
  194. assert container.get('HostConfig.BlkioDeviceReadIOps') == [{
  195. 'Path': '/dev/sda', 'Rate': 1000
  196. }]
  197. assert container.get('HostConfig.BlkioDeviceWriteIOps') == [{
  198. 'Path': '/dev/sda', 'Rate': 800
  199. }]
  200. def test_create_container_with_security_opt(self):
  201. security_opt = ['label:disable']
  202. service = self.create_service('db', security_opt=security_opt)
  203. container = service.create_container()
  204. service.start_container(container)
  205. self.assertEqual(set(container.get('HostConfig.SecurityOpt')), set(security_opt))
  206. # @pytest.mark.xfail(True, reason='Not supported on most drivers')
  207. @pytest.mark.skipif(True, reason='https://github.com/moby/moby/issues/34270')
  208. def test_create_container_with_storage_opt(self):
  209. storage_opt = {'size': '1G'}
  210. service = self.create_service('db', storage_opt=storage_opt)
  211. container = service.create_container()
  212. service.start_container(container)
  213. self.assertEqual(container.get('HostConfig.StorageOpt'), storage_opt)
  214. def test_create_container_with_mac_address(self):
  215. service = self.create_service('db', mac_address='02:42:ac:11:65:43')
  216. container = service.create_container()
  217. service.start_container(container)
  218. self.assertEqual(container.inspect()['Config']['MacAddress'], '02:42:ac:11:65:43')
  219. def test_create_container_with_specified_volume(self):
  220. host_path = '/tmp/host-path'
  221. container_path = '/container-path'
  222. service = self.create_service(
  223. 'db',
  224. volumes=[VolumeSpec(host_path, container_path, 'rw')])
  225. container = service.create_container()
  226. service.start_container(container)
  227. assert container.get_mount(container_path)
  228. # Match the last component ("host-path"), because boot2docker symlinks /tmp
  229. actual_host_path = container.get_mount(container_path)['Source']
  230. self.assertTrue(path.basename(actual_host_path) == path.basename(host_path),
  231. msg=("Last component differs: %s, %s" % (actual_host_path, host_path)))
  232. def test_recreate_preserves_volume_with_trailing_slash(self):
  233. """When the Compose file specifies a trailing slash in the container path, make
  234. sure we copy the volume over when recreating.
  235. """
  236. service = self.create_service('data', volumes=[VolumeSpec.parse('/data/')])
  237. old_container = create_and_start_container(service)
  238. volume_path = old_container.get_mount('/data')['Source']
  239. new_container = service.recreate_container(old_container)
  240. self.assertEqual(new_container.get_mount('/data')['Source'], volume_path)
  241. def test_duplicate_volume_trailing_slash(self):
  242. """
  243. When an image specifies a volume, and the Compose file specifies a host path
  244. but adds a trailing slash, make sure that we don't create duplicate binds.
  245. """
  246. host_path = '/tmp/data'
  247. container_path = '/data'
  248. volumes = [VolumeSpec.parse('{}:{}/'.format(host_path, container_path))]
  249. tmp_container = self.client.create_container(
  250. 'busybox', 'true',
  251. volumes={container_path: {}},
  252. labels={'com.docker.compose.test_image': 'true'},
  253. host_config={}
  254. )
  255. image = self.client.commit(tmp_container)['Id']
  256. service = self.create_service('db', image=image, volumes=volumes)
  257. old_container = create_and_start_container(service)
  258. self.assertEqual(
  259. old_container.get('Config.Volumes'),
  260. {container_path: {}},
  261. )
  262. service = self.create_service('db', image=image, volumes=volumes)
  263. new_container = service.recreate_container(old_container)
  264. self.assertEqual(
  265. new_container.get('Config.Volumes'),
  266. {container_path: {}},
  267. )
  268. self.assertEqual(service.containers(stopped=False), [new_container])
  269. def test_create_container_with_volumes_from(self):
  270. volume_service = self.create_service('data')
  271. volume_container_1 = volume_service.create_container()
  272. volume_container_2 = Container.create(
  273. self.client,
  274. image='busybox:latest',
  275. command=["top"],
  276. labels={LABEL_PROJECT: 'composetest'},
  277. host_config={},
  278. )
  279. host_service = self.create_service(
  280. 'host',
  281. volumes_from=[
  282. VolumeFromSpec(volume_service, 'rw', 'service'),
  283. VolumeFromSpec(volume_container_2, 'rw', 'container')
  284. ]
  285. )
  286. host_container = host_service.create_container()
  287. host_service.start_container(host_container)
  288. self.assertIn(volume_container_1.id + ':rw',
  289. host_container.get('HostConfig.VolumesFrom'))
  290. self.assertIn(volume_container_2.id + ':rw',
  291. host_container.get('HostConfig.VolumesFrom'))
  292. def test_execute_convergence_plan_recreate(self):
  293. service = self.create_service(
  294. 'db',
  295. environment={'FOO': '1'},
  296. volumes=[VolumeSpec.parse('/etc')],
  297. entrypoint=['top'],
  298. command=['-d', '1']
  299. )
  300. old_container = service.create_container()
  301. self.assertEqual(old_container.get('Config.Entrypoint'), ['top'])
  302. self.assertEqual(old_container.get('Config.Cmd'), ['-d', '1'])
  303. self.assertIn('FOO=1', old_container.get('Config.Env'))
  304. self.assertEqual(old_container.name, 'composetest_db_1')
  305. service.start_container(old_container)
  306. old_container.inspect() # reload volume data
  307. volume_path = old_container.get_mount('/etc')['Source']
  308. num_containers_before = len(self.client.containers(all=True))
  309. service.options['environment']['FOO'] = '2'
  310. new_container, = service.execute_convergence_plan(
  311. ConvergencePlan('recreate', [old_container]))
  312. self.assertEqual(new_container.get('Config.Entrypoint'), ['top'])
  313. self.assertEqual(new_container.get('Config.Cmd'), ['-d', '1'])
  314. self.assertIn('FOO=2', new_container.get('Config.Env'))
  315. self.assertEqual(new_container.name, 'composetest_db_1')
  316. self.assertEqual(new_container.get_mount('/etc')['Source'], volume_path)
  317. if not is_cluster(self.client):
  318. assert (
  319. 'affinity:container==%s' % old_container.id in
  320. new_container.get('Config.Env')
  321. )
  322. else:
  323. # In Swarm, the env marker is consumed and the container should be deployed
  324. # on the same node.
  325. assert old_container.get('Node.Name') == new_container.get('Node.Name')
  326. self.assertEqual(len(self.client.containers(all=True)), num_containers_before)
  327. self.assertNotEqual(old_container.id, new_container.id)
  328. self.assertRaises(APIError,
  329. self.client.inspect_container,
  330. old_container.id)
  331. def test_execute_convergence_plan_recreate_twice(self):
  332. service = self.create_service(
  333. 'db',
  334. volumes=[VolumeSpec.parse('/etc')],
  335. entrypoint=['top'],
  336. command=['-d', '1'])
  337. orig_container = service.create_container()
  338. service.start_container(orig_container)
  339. orig_container.inspect() # reload volume data
  340. volume_path = orig_container.get_mount('/etc')['Source']
  341. # Do this twice to reproduce the bug
  342. for _ in range(2):
  343. new_container, = service.execute_convergence_plan(
  344. ConvergencePlan('recreate', [orig_container]))
  345. assert new_container.get_mount('/etc')['Source'] == volume_path
  346. if not is_cluster(self.client):
  347. assert ('affinity:container==%s' % orig_container.id in
  348. new_container.get('Config.Env'))
  349. else:
  350. # In Swarm, the env marker is consumed and the container should be deployed
  351. # on the same node.
  352. assert orig_container.get('Node.Name') == new_container.get('Node.Name')
  353. orig_container = new_container
  354. def test_execute_convergence_plan_when_containers_are_stopped(self):
  355. service = self.create_service(
  356. 'db',
  357. environment={'FOO': '1'},
  358. volumes=[VolumeSpec.parse('/var/db')],
  359. entrypoint=['top'],
  360. command=['-d', '1']
  361. )
  362. service.create_container()
  363. containers = service.containers(stopped=True)
  364. self.assertEqual(len(containers), 1)
  365. container, = containers
  366. self.assertFalse(container.is_running)
  367. service.execute_convergence_plan(ConvergencePlan('start', [container]))
  368. containers = service.containers()
  369. self.assertEqual(len(containers), 1)
  370. container.inspect()
  371. self.assertEqual(container, containers[0])
  372. self.assertTrue(container.is_running)
  373. def test_execute_convergence_plan_with_image_declared_volume(self):
  374. service = Service(
  375. project='composetest',
  376. name='db',
  377. client=self.client,
  378. build={'context': 'tests/fixtures/dockerfile-with-volume'},
  379. )
  380. old_container = create_and_start_container(service)
  381. self.assertEqual(
  382. [mount['Destination'] for mount in old_container.get('Mounts')], ['/data']
  383. )
  384. volume_path = old_container.get_mount('/data')['Source']
  385. new_container, = service.execute_convergence_plan(
  386. ConvergencePlan('recreate', [old_container]))
  387. self.assertEqual(
  388. [mount['Destination'] for mount in new_container.get('Mounts')],
  389. ['/data']
  390. )
  391. self.assertEqual(new_container.get_mount('/data')['Source'], volume_path)
  392. def test_execute_convergence_plan_when_image_volume_masks_config(self):
  393. service = self.create_service(
  394. 'db',
  395. build={'context': 'tests/fixtures/dockerfile-with-volume'},
  396. )
  397. old_container = create_and_start_container(service)
  398. self.assertEqual(
  399. [mount['Destination'] for mount in old_container.get('Mounts')],
  400. ['/data']
  401. )
  402. volume_path = old_container.get_mount('/data')['Source']
  403. service.options['volumes'] = [VolumeSpec.parse('/tmp:/data')]
  404. with mock.patch('compose.service.log') as mock_log:
  405. new_container, = service.execute_convergence_plan(
  406. ConvergencePlan('recreate', [old_container]))
  407. mock_log.warn.assert_called_once_with(mock.ANY)
  408. _, args, kwargs = mock_log.warn.mock_calls[0]
  409. self.assertIn(
  410. "Service \"db\" is using volume \"/data\" from the previous container",
  411. args[0])
  412. self.assertEqual(
  413. [mount['Destination'] for mount in new_container.get('Mounts')],
  414. ['/data']
  415. )
  416. self.assertEqual(new_container.get_mount('/data')['Source'], volume_path)
  417. def test_execute_convergence_plan_when_host_volume_is_removed(self):
  418. host_path = '/tmp/host-path'
  419. service = self.create_service(
  420. 'db',
  421. build={'context': 'tests/fixtures/dockerfile-with-volume'},
  422. volumes=[VolumeSpec(host_path, '/data', 'rw')])
  423. old_container = create_and_start_container(service)
  424. assert (
  425. [mount['Destination'] for mount in old_container.get('Mounts')] ==
  426. ['/data']
  427. )
  428. service.options['volumes'] = []
  429. with mock.patch('compose.service.log', autospec=True) as mock_log:
  430. new_container, = service.execute_convergence_plan(
  431. ConvergencePlan('recreate', [old_container]))
  432. assert not mock_log.warn.called
  433. assert (
  434. [mount['Destination'] for mount in new_container.get('Mounts')] ==
  435. ['/data']
  436. )
  437. assert new_container.get_mount('/data')['Source'] != host_path
  438. def test_execute_convergence_plan_without_start(self):
  439. service = self.create_service(
  440. 'db',
  441. build={'context': 'tests/fixtures/dockerfile-with-volume'}
  442. )
  443. containers = service.execute_convergence_plan(ConvergencePlan('create', []), start=False)
  444. service_containers = service.containers(stopped=True)
  445. assert len(service_containers) == 1
  446. assert not service_containers[0].is_running
  447. containers = service.execute_convergence_plan(
  448. ConvergencePlan('recreate', containers),
  449. start=False)
  450. service_containers = service.containers(stopped=True)
  451. assert len(service_containers) == 1
  452. assert not service_containers[0].is_running
  453. service.execute_convergence_plan(ConvergencePlan('start', containers), start=False)
  454. service_containers = service.containers(stopped=True)
  455. assert len(service_containers) == 1
  456. assert not service_containers[0].is_running
  457. def test_start_container_passes_through_options(self):
  458. db = self.create_service('db')
  459. create_and_start_container(db, environment={'FOO': 'BAR'})
  460. self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
  461. def test_start_container_inherits_options_from_constructor(self):
  462. db = self.create_service('db', environment={'FOO': 'BAR'})
  463. create_and_start_container(db)
  464. self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
  465. @no_cluster('No legacy links support in Swarm')
  466. def test_start_container_creates_links(self):
  467. db = self.create_service('db')
  468. web = self.create_service('web', links=[(db, None)])
  469. create_and_start_container(db)
  470. create_and_start_container(db)
  471. create_and_start_container(web)
  472. self.assertEqual(
  473. set(get_links(web.containers()[0])),
  474. set([
  475. 'composetest_db_1', 'db_1',
  476. 'composetest_db_2', 'db_2',
  477. 'db'])
  478. )
  479. @no_cluster('No legacy links support in Swarm')
  480. def test_start_container_creates_links_with_names(self):
  481. db = self.create_service('db')
  482. web = self.create_service('web', links=[(db, 'custom_link_name')])
  483. create_and_start_container(db)
  484. create_and_start_container(db)
  485. create_and_start_container(web)
  486. self.assertEqual(
  487. set(get_links(web.containers()[0])),
  488. set([
  489. 'composetest_db_1', 'db_1',
  490. 'composetest_db_2', 'db_2',
  491. 'custom_link_name'])
  492. )
  493. @no_cluster('No legacy links support in Swarm')
  494. def test_start_container_with_external_links(self):
  495. db = self.create_service('db')
  496. web = self.create_service('web', external_links=['composetest_db_1',
  497. 'composetest_db_2',
  498. 'composetest_db_3:db_3'])
  499. for _ in range(3):
  500. create_and_start_container(db)
  501. create_and_start_container(web)
  502. self.assertEqual(
  503. set(get_links(web.containers()[0])),
  504. set([
  505. 'composetest_db_1',
  506. 'composetest_db_2',
  507. 'db_3']),
  508. )
  509. @no_cluster('No legacy links support in Swarm')
  510. def test_start_normal_container_does_not_create_links_to_its_own_service(self):
  511. db = self.create_service('db')
  512. create_and_start_container(db)
  513. create_and_start_container(db)
  514. c = create_and_start_container(db)
  515. self.assertEqual(set(get_links(c)), set([]))
  516. @no_cluster('No legacy links support in Swarm')
  517. def test_start_one_off_container_creates_links_to_its_own_service(self):
  518. db = self.create_service('db')
  519. create_and_start_container(db)
  520. create_and_start_container(db)
  521. c = create_and_start_container(db, one_off=OneOffFilter.only)
  522. self.assertEqual(
  523. set(get_links(c)),
  524. set([
  525. 'composetest_db_1', 'db_1',
  526. 'composetest_db_2', 'db_2',
  527. 'db'])
  528. )
  529. def test_start_container_builds_images(self):
  530. service = Service(
  531. name='test',
  532. client=self.client,
  533. build={'context': 'tests/fixtures/simple-dockerfile'},
  534. project='composetest',
  535. )
  536. container = create_and_start_container(service)
  537. container.wait()
  538. self.assertIn(b'success', container.logs())
  539. assert len(self.client.images(name='composetest_test')) >= 1
  540. def test_start_container_uses_tagged_image_if_it_exists(self):
  541. self.check_build('tests/fixtures/simple-dockerfile', tag='composetest_test')
  542. service = Service(
  543. name='test',
  544. client=self.client,
  545. build={'context': 'this/does/not/exist/and/will/throw/error'},
  546. project='composetest',
  547. )
  548. container = create_and_start_container(service)
  549. container.wait()
  550. self.assertIn(b'success', container.logs())
  551. def test_start_container_creates_ports(self):
  552. service = self.create_service('web', ports=[8000])
  553. container = create_and_start_container(service).inspect()
  554. self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/tcp'])
  555. self.assertNotEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000')
  556. def test_build(self):
  557. base_dir = tempfile.mkdtemp()
  558. self.addCleanup(shutil.rmtree, base_dir)
  559. with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
  560. f.write("FROM busybox\n")
  561. service = self.create_service('web', build={'context': base_dir})
  562. service.build()
  563. self.addCleanup(self.client.remove_image, service.image_name)
  564. assert self.client.inspect_image('composetest_web')
  565. def test_build_non_ascii_filename(self):
  566. base_dir = tempfile.mkdtemp()
  567. self.addCleanup(shutil.rmtree, base_dir)
  568. with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
  569. f.write("FROM busybox\n")
  570. with open(os.path.join(base_dir.encode('utf8'), b'foo\xE2bar'), 'w') as f:
  571. f.write("hello world\n")
  572. service = self.create_service('web', build={'context': text_type(base_dir)})
  573. service.build()
  574. self.addCleanup(self.client.remove_image, service.image_name)
  575. assert self.client.inspect_image('composetest_web')
  576. def test_build_with_image_name(self):
  577. base_dir = tempfile.mkdtemp()
  578. self.addCleanup(shutil.rmtree, base_dir)
  579. with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
  580. f.write("FROM busybox\n")
  581. image_name = 'examples/composetest:latest'
  582. self.addCleanup(self.client.remove_image, image_name)
  583. self.create_service('web', build={'context': base_dir}, image=image_name).build()
  584. assert self.client.inspect_image(image_name)
  585. def test_build_with_git_url(self):
  586. build_url = "https://github.com/dnephin/docker-build-from-url.git"
  587. service = self.create_service('buildwithurl', build={'context': build_url})
  588. self.addCleanup(self.client.remove_image, service.image_name)
  589. service.build()
  590. assert service.image()
  591. def test_build_with_build_args(self):
  592. base_dir = tempfile.mkdtemp()
  593. self.addCleanup(shutil.rmtree, base_dir)
  594. with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
  595. f.write("FROM busybox\n")
  596. f.write("ARG build_version\n")
  597. f.write("RUN echo ${build_version}\n")
  598. service = self.create_service('buildwithargs',
  599. build={'context': text_type(base_dir),
  600. 'args': {"build_version": "1"}})
  601. service.build()
  602. self.addCleanup(self.client.remove_image, service.image_name)
  603. assert service.image()
  604. assert "build_version=1" in service.image()['ContainerConfig']['Cmd']
  605. def test_build_with_build_args_override(self):
  606. base_dir = tempfile.mkdtemp()
  607. self.addCleanup(shutil.rmtree, base_dir)
  608. with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
  609. f.write("FROM busybox\n")
  610. f.write("ARG build_version\n")
  611. f.write("RUN echo ${build_version}\n")
  612. service = self.create_service('buildwithargs',
  613. build={'context': text_type(base_dir),
  614. 'args': {"build_version": "1"}})
  615. service.build(build_args_override={'build_version': '2'})
  616. self.addCleanup(self.client.remove_image, service.image_name)
  617. assert service.image()
  618. assert "build_version=2" in service.image()['ContainerConfig']['Cmd']
  619. def test_build_with_build_labels(self):
  620. base_dir = tempfile.mkdtemp()
  621. self.addCleanup(shutil.rmtree, base_dir)
  622. with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
  623. f.write('FROM busybox\n')
  624. service = self.create_service('buildlabels', build={
  625. 'context': text_type(base_dir),
  626. 'labels': {'com.docker.compose.test': 'true'}
  627. })
  628. service.build()
  629. self.addCleanup(self.client.remove_image, service.image_name)
  630. assert service.image()
  631. assert service.image()['Config']['Labels']['com.docker.compose.test'] == 'true'
  632. @no_cluster('Container networks not on Swarm')
  633. def test_build_with_network(self):
  634. base_dir = tempfile.mkdtemp()
  635. self.addCleanup(shutil.rmtree, base_dir)
  636. with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
  637. f.write('FROM busybox\n')
  638. f.write('RUN ping -c1 google.local\n')
  639. net_container = self.client.create_container(
  640. 'busybox', 'top', host_config=self.client.create_host_config(
  641. extra_hosts={'google.local': '8.8.8.8'}
  642. ), name='composetest_build_network'
  643. )
  644. self.addCleanup(self.client.remove_container, net_container, force=True)
  645. self.client.start(net_container)
  646. service = self.create_service('buildwithnet', build={
  647. 'context': text_type(base_dir),
  648. 'network': 'container:{}'.format(net_container['Id'])
  649. })
  650. service.build()
  651. self.addCleanup(self.client.remove_image, service.image_name)
  652. assert service.image()
  653. @v2_3_only()
  654. def test_build_with_target(self):
  655. self.require_api_version('1.30')
  656. base_dir = tempfile.mkdtemp()
  657. self.addCleanup(shutil.rmtree, base_dir)
  658. with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
  659. f.write('FROM busybox as one\n')
  660. f.write('LABEL com.docker.compose.test.target=one\n')
  661. f.write('FROM busybox as two\n')
  662. f.write('LABEL com.docker.compose.test.target=two\n')
  663. service = self.create_service('buildlabels', build={
  664. 'context': text_type(base_dir),
  665. 'target': 'one'
  666. })
  667. service.build()
  668. assert service.image()
  669. assert service.image()['Config']['Labels']['com.docker.compose.test.target'] == 'one'
  670. def test_start_container_stays_unprivileged(self):
  671. service = self.create_service('web')
  672. container = create_and_start_container(service).inspect()
  673. self.assertEqual(container['HostConfig']['Privileged'], False)
  674. def test_start_container_becomes_privileged(self):
  675. service = self.create_service('web', privileged=True)
  676. container = create_and_start_container(service).inspect()
  677. self.assertEqual(container['HostConfig']['Privileged'], True)
  678. def test_expose_does_not_publish_ports(self):
  679. service = self.create_service('web', expose=["8000"])
  680. container = create_and_start_container(service).inspect()
  681. self.assertEqual(container['NetworkSettings']['Ports'], {'8000/tcp': None})
  682. def test_start_container_creates_port_with_explicit_protocol(self):
  683. service = self.create_service('web', ports=['8000/udp'])
  684. container = create_and_start_container(service).inspect()
  685. self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/udp'])
  686. def test_start_container_creates_fixed_external_ports(self):
  687. service = self.create_service('web', ports=['8000:8000'])
  688. container = create_and_start_container(service).inspect()
  689. self.assertIn('8000/tcp', container['NetworkSettings']['Ports'])
  690. self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000')
  691. def test_start_container_creates_fixed_external_ports_when_it_is_different_to_internal_port(self):
  692. service = self.create_service('web', ports=['8001:8000'])
  693. container = create_and_start_container(service).inspect()
  694. self.assertIn('8000/tcp', container['NetworkSettings']['Ports'])
  695. self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8001')
  696. def test_port_with_explicit_interface(self):
  697. service = self.create_service('web', ports=[
  698. '127.0.0.1:8001:8000',
  699. '0.0.0.0:9001:9000/udp',
  700. ])
  701. container = create_and_start_container(service).inspect()
  702. assert container['NetworkSettings']['Ports']['8000/tcp'] == [{
  703. 'HostIp': '127.0.0.1',
  704. 'HostPort': '8001',
  705. }]
  706. assert container['NetworkSettings']['Ports']['9000/udp'][0]['HostPort'] == '9001'
  707. if not is_cluster(self.client):
  708. assert container['NetworkSettings']['Ports']['9000/udp'][0]['HostIp'] == '0.0.0.0'
  709. # self.assertEqual(container['NetworkSettings']['Ports'], {
  710. # '8000/tcp': [
  711. # {
  712. # 'HostIp': '127.0.0.1',
  713. # 'HostPort': '8001',
  714. # },
  715. # ],
  716. # '9000/udp': [
  717. # {
  718. # 'HostIp': '0.0.0.0',
  719. # 'HostPort': '9001',
  720. # },
  721. # ],
  722. # })
  723. def test_create_with_image_id(self):
  724. # Get image id for the current busybox:latest
  725. pull_busybox(self.client)
  726. image_id = self.client.inspect_image('busybox:latest')['Id'][:12]
  727. service = self.create_service('foo', image=image_id)
  728. service.create_container()
  729. def test_scale(self):
  730. service = self.create_service('web')
  731. service.scale(1)
  732. self.assertEqual(len(service.containers()), 1)
  733. # Ensure containers don't have stdout or stdin connected
  734. container = service.containers()[0]
  735. config = container.inspect()['Config']
  736. self.assertFalse(config['AttachStderr'])
  737. self.assertFalse(config['AttachStdout'])
  738. self.assertFalse(config['AttachStdin'])
  739. service.scale(3)
  740. self.assertEqual(len(service.containers()), 3)
  741. service.scale(1)
  742. self.assertEqual(len(service.containers()), 1)
  743. service.scale(0)
  744. self.assertEqual(len(service.containers()), 0)
  745. @pytest.mark.skipif(
  746. SWARM_SKIP_CONTAINERS_ALL,
  747. reason='Swarm /containers/json bug'
  748. )
  749. def test_scale_with_stopped_containers(self):
  750. """
  751. Given there are some stopped containers and scale is called with a
  752. desired number that is the same as the number of stopped containers,
  753. test that those containers are restarted and not removed/recreated.
  754. """
  755. service = self.create_service('web')
  756. next_number = service._next_container_number()
  757. valid_numbers = [next_number, next_number + 1]
  758. service.create_container(number=next_number)
  759. service.create_container(number=next_number + 1)
  760. with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr:
  761. service.scale(2)
  762. for container in service.containers():
  763. self.assertTrue(container.is_running)
  764. self.assertTrue(container.number in valid_numbers)
  765. captured_output = mock_stderr.getvalue()
  766. self.assertNotIn('Creating', captured_output)
  767. self.assertIn('Starting', captured_output)
  768. def test_scale_with_stopped_containers_and_needing_creation(self):
  769. """
  770. Given there are some stopped containers and scale is called with a
  771. desired number that is greater than the number of stopped containers,
  772. test that those containers are restarted and required number are created.
  773. """
  774. service = self.create_service('web')
  775. next_number = service._next_container_number()
  776. service.create_container(number=next_number, quiet=True)
  777. for container in service.containers():
  778. self.assertFalse(container.is_running)
  779. with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr:
  780. service.scale(2)
  781. self.assertEqual(len(service.containers()), 2)
  782. for container in service.containers():
  783. self.assertTrue(container.is_running)
  784. captured_output = mock_stderr.getvalue()
  785. self.assertIn('Creating', captured_output)
  786. self.assertIn('Starting', captured_output)
  787. def test_scale_with_api_error(self):
  788. """Test that when scaling if the API returns an error, that error is handled
  789. and the remaining threads continue.
  790. """
  791. service = self.create_service('web')
  792. next_number = service._next_container_number()
  793. service.create_container(number=next_number, quiet=True)
  794. with mock.patch(
  795. 'compose.container.Container.create',
  796. side_effect=APIError(
  797. message="testing",
  798. response={},
  799. explanation="Boom")):
  800. with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr:
  801. with pytest.raises(OperationFailedError):
  802. service.scale(3)
  803. assert len(service.containers()) == 1
  804. assert service.containers()[0].is_running
  805. assert (
  806. "ERROR: for composetest_web_2 Cannot create container for service"
  807. " web: Boom" in mock_stderr.getvalue()
  808. )
  809. def test_scale_with_unexpected_exception(self):
  810. """Test that when scaling if the API returns an error, that is not of type
  811. APIError, that error is re-raised.
  812. """
  813. service = self.create_service('web')
  814. next_number = service._next_container_number()
  815. service.create_container(number=next_number, quiet=True)
  816. with mock.patch(
  817. 'compose.container.Container.create',
  818. side_effect=ValueError("BOOM")
  819. ):
  820. with self.assertRaises(ValueError):
  821. service.scale(3)
  822. self.assertEqual(len(service.containers()), 1)
  823. self.assertTrue(service.containers()[0].is_running)
  824. @mock.patch('compose.service.log')
  825. def test_scale_with_desired_number_already_achieved(self, mock_log):
  826. """
  827. Test that calling scale with a desired number that is equal to the
  828. number of containers already running results in no change.
  829. """
  830. service = self.create_service('web')
  831. next_number = service._next_container_number()
  832. container = service.create_container(number=next_number, quiet=True)
  833. container.start()
  834. container.inspect()
  835. assert container.is_running
  836. assert len(service.containers()) == 1
  837. service.scale(1)
  838. assert len(service.containers()) == 1
  839. container.inspect()
  840. assert container.is_running
  841. captured_output = mock_log.info.call_args[0]
  842. assert 'Desired container number already achieved' in captured_output
  843. @mock.patch('compose.service.log')
  844. def test_scale_with_custom_container_name_outputs_warning(self, mock_log):
  845. """Test that calling scale on a service that has a custom container name
  846. results in warning output.
  847. """
  848. service = self.create_service('app', container_name='custom-container')
  849. self.assertEqual(service.custom_container_name, 'custom-container')
  850. with pytest.raises(OperationFailedError):
  851. service.scale(3)
  852. captured_output = mock_log.warn.call_args[0][0]
  853. self.assertEqual(len(service.containers()), 1)
  854. self.assertIn(
  855. "Remove the custom name to scale the service.",
  856. captured_output
  857. )
  858. def test_scale_sets_ports(self):
  859. service = self.create_service('web', ports=['8000'])
  860. service.scale(2)
  861. containers = service.containers()
  862. self.assertEqual(len(containers), 2)
  863. for container in containers:
  864. self.assertEqual(
  865. list(container.get('HostConfig.PortBindings')),
  866. ['8000/tcp'])
  867. def test_scale_with_immediate_exit(self):
  868. service = self.create_service('web', image='busybox', command='true')
  869. service.scale(2)
  870. assert len(service.containers(stopped=True)) == 2
  871. def test_network_mode_none(self):
  872. service = self.create_service('web', network_mode=NetworkMode('none'))
  873. container = create_and_start_container(service)
  874. self.assertEqual(container.get('HostConfig.NetworkMode'), 'none')
  875. def test_network_mode_bridged(self):
  876. service = self.create_service('web', network_mode=NetworkMode('bridge'))
  877. container = create_and_start_container(service)
  878. self.assertEqual(container.get('HostConfig.NetworkMode'), 'bridge')
  879. def test_network_mode_host(self):
  880. service = self.create_service('web', network_mode=NetworkMode('host'))
  881. container = create_and_start_container(service)
  882. self.assertEqual(container.get('HostConfig.NetworkMode'), 'host')
  883. def test_pid_mode_none_defined(self):
  884. service = self.create_service('web', pid_mode=None)
  885. container = create_and_start_container(service)
  886. self.assertEqual(container.get('HostConfig.PidMode'), '')
  887. def test_pid_mode_host(self):
  888. service = self.create_service('web', pid_mode=PidMode('host'))
  889. container = create_and_start_container(service)
  890. self.assertEqual(container.get('HostConfig.PidMode'), 'host')
  891. @v2_1_only()
  892. def test_userns_mode_none_defined(self):
  893. service = self.create_service('web', userns_mode=None)
  894. container = create_and_start_container(service)
  895. self.assertEqual(container.get('HostConfig.UsernsMode'), '')
  896. @v2_1_only()
  897. def test_userns_mode_host(self):
  898. service = self.create_service('web', userns_mode='host')
  899. container = create_and_start_container(service)
  900. self.assertEqual(container.get('HostConfig.UsernsMode'), 'host')
  901. def test_dns_no_value(self):
  902. service = self.create_service('web')
  903. container = create_and_start_container(service)
  904. self.assertIsNone(container.get('HostConfig.Dns'))
  905. def test_dns_list(self):
  906. service = self.create_service('web', dns=['8.8.8.8', '9.9.9.9'])
  907. container = create_and_start_container(service)
  908. self.assertEqual(container.get('HostConfig.Dns'), ['8.8.8.8', '9.9.9.9'])
  909. def test_mem_swappiness(self):
  910. service = self.create_service('web', mem_swappiness=11)
  911. container = create_and_start_container(service)
  912. self.assertEqual(container.get('HostConfig.MemorySwappiness'), 11)
  913. def test_mem_reservation(self):
  914. service = self.create_service('web', mem_reservation='20m')
  915. container = create_and_start_container(service)
  916. assert container.get('HostConfig.MemoryReservation') == 20 * 1024 * 1024
  917. def test_restart_always_value(self):
  918. service = self.create_service('web', restart={'Name': 'always'})
  919. container = create_and_start_container(service)
  920. self.assertEqual(container.get('HostConfig.RestartPolicy.Name'), 'always')
  921. def test_oom_score_adj_value(self):
  922. service = self.create_service('web', oom_score_adj=500)
  923. container = create_and_start_container(service)
  924. self.assertEqual(container.get('HostConfig.OomScoreAdj'), 500)
  925. def test_group_add_value(self):
  926. service = self.create_service('web', group_add=["root", "1"])
  927. container = create_and_start_container(service)
  928. host_container_groupadd = container.get('HostConfig.GroupAdd')
  929. assert "root" in host_container_groupadd
  930. assert "1" in host_container_groupadd
  931. def test_dns_opt_value(self):
  932. service = self.create_service('web', dns_opt=["use-vc", "no-tld-query"])
  933. container = create_and_start_container(service)
  934. dns_opt = container.get('HostConfig.DnsOptions')
  935. assert 'use-vc' in dns_opt
  936. assert 'no-tld-query' in dns_opt
  937. def test_restart_on_failure_value(self):
  938. service = self.create_service('web', restart={
  939. 'Name': 'on-failure',
  940. 'MaximumRetryCount': 5
  941. })
  942. container = create_and_start_container(service)
  943. self.assertEqual(container.get('HostConfig.RestartPolicy.Name'), 'on-failure')
  944. self.assertEqual(container.get('HostConfig.RestartPolicy.MaximumRetryCount'), 5)
  945. def test_cap_add_list(self):
  946. service = self.create_service('web', cap_add=['SYS_ADMIN', 'NET_ADMIN'])
  947. container = create_and_start_container(service)
  948. self.assertEqual(container.get('HostConfig.CapAdd'), ['SYS_ADMIN', 'NET_ADMIN'])
  949. def test_cap_drop_list(self):
  950. service = self.create_service('web', cap_drop=['SYS_ADMIN', 'NET_ADMIN'])
  951. container = create_and_start_container(service)
  952. self.assertEqual(container.get('HostConfig.CapDrop'), ['SYS_ADMIN', 'NET_ADMIN'])
  953. def test_dns_search(self):
  954. service = self.create_service('web', dns_search=['dc1.example.com', 'dc2.example.com'])
  955. container = create_and_start_container(service)
  956. self.assertEqual(container.get('HostConfig.DnsSearch'), ['dc1.example.com', 'dc2.example.com'])
  957. @v2_only()
  958. def test_tmpfs(self):
  959. service = self.create_service('web', tmpfs=['/run'])
  960. container = create_and_start_container(service)
  961. self.assertEqual(container.get('HostConfig.Tmpfs'), {'/run': ''})
  962. def test_working_dir_param(self):
  963. service = self.create_service('container', working_dir='/working/dir/sample')
  964. container = service.create_container()
  965. self.assertEqual(container.get('Config.WorkingDir'), '/working/dir/sample')
  966. def test_split_env(self):
  967. service = self.create_service(
  968. 'web',
  969. environment=['NORMAL=F1', 'CONTAINS_EQUALS=F=2', 'TRAILING_EQUALS='])
  970. env = create_and_start_container(service).environment
  971. for k, v in {'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''}.items():
  972. self.assertEqual(env[k], v)
  973. def test_env_from_file_combined_with_env(self):
  974. service = self.create_service(
  975. 'web',
  976. environment=['ONE=1', 'TWO=2', 'THREE=3'],
  977. env_file=['tests/fixtures/env/one.env', 'tests/fixtures/env/two.env'])
  978. env = create_and_start_container(service).environment
  979. for k, v in {
  980. 'ONE': '1',
  981. 'TWO': '2',
  982. 'THREE': '3',
  983. 'FOO': 'baz',
  984. 'DOO': 'dah'
  985. }.items():
  986. self.assertEqual(env[k], v)
  987. @v3_only()
  988. def test_build_with_cachefrom(self):
  989. base_dir = tempfile.mkdtemp()
  990. self.addCleanup(shutil.rmtree, base_dir)
  991. with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
  992. f.write("FROM busybox\n")
  993. service = self.create_service('cache_from',
  994. build={'context': base_dir,
  995. 'cache_from': ['build1']})
  996. service.build()
  997. self.addCleanup(self.client.remove_image, service.image_name)
  998. assert service.image()
  999. @mock.patch.dict(os.environ)
  1000. def test_resolve_env(self):
  1001. os.environ['FILE_DEF'] = 'E1'
  1002. os.environ['FILE_DEF_EMPTY'] = 'E2'
  1003. os.environ['ENV_DEF'] = 'E3'
  1004. service = self.create_service(
  1005. 'web',
  1006. environment={
  1007. 'FILE_DEF': 'F1',
  1008. 'FILE_DEF_EMPTY': '',
  1009. 'ENV_DEF': None,
  1010. 'NO_DEF': None
  1011. }
  1012. )
  1013. env = create_and_start_container(service).environment
  1014. for k, v in {
  1015. 'FILE_DEF': 'F1',
  1016. 'FILE_DEF_EMPTY': '',
  1017. 'ENV_DEF': 'E3',
  1018. 'NO_DEF': None
  1019. }.items():
  1020. self.assertEqual(env[k], v)
  1021. def test_with_high_enough_api_version_we_get_default_network_mode(self):
  1022. # TODO: remove this test once minimum docker version is 1.8.x
  1023. with mock.patch.object(self.client, '_version', '1.20'):
  1024. service = self.create_service('web')
  1025. service_config = service._get_container_host_config({})
  1026. self.assertEqual(service_config['NetworkMode'], 'default')
  1027. def test_labels(self):
  1028. labels_dict = {
  1029. 'com.example.description': "Accounting webapp",
  1030. 'com.example.department': "Finance",
  1031. 'com.example.label-with-empty-value': "",
  1032. }
  1033. compose_labels = {
  1034. LABEL_CONTAINER_NUMBER: '1',
  1035. LABEL_ONE_OFF: 'False',
  1036. LABEL_PROJECT: 'composetest',
  1037. LABEL_SERVICE: 'web',
  1038. LABEL_VERSION: __version__,
  1039. }
  1040. expected = dict(labels_dict, **compose_labels)
  1041. service = self.create_service('web', labels=labels_dict)
  1042. labels = create_and_start_container(service).labels.items()
  1043. for pair in expected.items():
  1044. self.assertIn(pair, labels)
  1045. def test_empty_labels(self):
  1046. labels_dict = {'foo': '', 'bar': ''}
  1047. service = self.create_service('web', labels=labels_dict)
  1048. labels = create_and_start_container(service).labels.items()
  1049. for name in labels_dict:
  1050. self.assertIn((name, ''), labels)
  1051. def test_stop_signal(self):
  1052. stop_signal = 'SIGINT'
  1053. service = self.create_service('web', stop_signal=stop_signal)
  1054. container = create_and_start_container(service)
  1055. self.assertEqual(container.stop_signal, stop_signal)
  1056. def test_custom_container_name(self):
  1057. service = self.create_service('web', container_name='my-web-container')
  1058. self.assertEqual(service.custom_container_name, 'my-web-container')
  1059. container = create_and_start_container(service)
  1060. self.assertEqual(container.name, 'my-web-container')
  1061. one_off_container = service.create_container(one_off=True)
  1062. self.assertNotEqual(one_off_container.name, 'my-web-container')
  1063. @pytest.mark.skipif(True, reason="Broken on 1.11.0 - 17.03.0")
  1064. def test_log_drive_invalid(self):
  1065. service = self.create_service('web', logging={'driver': 'xxx'})
  1066. expected_error_msg = "logger: no log driver named 'xxx' is registered"
  1067. with self.assertRaisesRegexp(APIError, expected_error_msg):
  1068. create_and_start_container(service)
  1069. def test_log_drive_empty_default_jsonfile(self):
  1070. service = self.create_service('web')
  1071. log_config = create_and_start_container(service).log_config
  1072. self.assertEqual('json-file', log_config['Type'])
  1073. self.assertFalse(log_config['Config'])
  1074. def test_log_drive_none(self):
  1075. service = self.create_service('web', logging={'driver': 'none'})
  1076. log_config = create_and_start_container(service).log_config
  1077. self.assertEqual('none', log_config['Type'])
  1078. self.assertFalse(log_config['Config'])
  1079. def test_devices(self):
  1080. service = self.create_service('web', devices=["/dev/random:/dev/mapped-random"])
  1081. device_config = create_and_start_container(service).get('HostConfig.Devices')
  1082. device_dict = {
  1083. 'PathOnHost': '/dev/random',
  1084. 'CgroupPermissions': 'rwm',
  1085. 'PathInContainer': '/dev/mapped-random'
  1086. }
  1087. self.assertEqual(1, len(device_config))
  1088. self.assertDictEqual(device_dict, device_config[0])
  1089. def test_duplicate_containers(self):
  1090. service = self.create_service('web')
  1091. options = service._get_container_create_options({}, 1)
  1092. original = Container.create(service.client, **options)
  1093. self.assertEqual(set(service.containers(stopped=True)), set([original]))
  1094. self.assertEqual(set(service.duplicate_containers()), set())
  1095. options['name'] = 'temporary_container_name'
  1096. duplicate = Container.create(service.client, **options)
  1097. self.assertEqual(set(service.containers(stopped=True)), set([original, duplicate]))
  1098. self.assertEqual(set(service.duplicate_containers()), set([duplicate]))
  1099. def converge(service, strategy=ConvergenceStrategy.changed):
  1100. """Create a converge plan from a strategy and execute the plan."""
  1101. plan = service.convergence_plan(strategy)
  1102. return service.execute_convergence_plan(plan, timeout=1)
  1103. class ConfigHashTest(DockerClientTestCase):
  1104. def test_no_config_hash_when_one_off(self):
  1105. web = self.create_service('web')
  1106. container = web.create_container(one_off=True)
  1107. self.assertNotIn(LABEL_CONFIG_HASH, container.labels)
  1108. def test_no_config_hash_when_overriding_options(self):
  1109. web = self.create_service('web')
  1110. container = web.create_container(environment={'FOO': '1'})
  1111. self.assertNotIn(LABEL_CONFIG_HASH, container.labels)
  1112. def test_config_hash_with_custom_labels(self):
  1113. web = self.create_service('web', labels={'foo': '1'})
  1114. container = converge(web)[0]
  1115. self.assertIn(LABEL_CONFIG_HASH, container.labels)
  1116. self.assertIn('foo', container.labels)
  1117. def test_config_hash_sticks_around(self):
  1118. web = self.create_service('web', command=["top"])
  1119. container = converge(web)[0]
  1120. self.assertIn(LABEL_CONFIG_HASH, container.labels)
  1121. web = self.create_service('web', command=["top", "-d", "1"])
  1122. container = converge(web)[0]
  1123. self.assertIn(LABEL_CONFIG_HASH, container.labels)