service_test.py 40 KB

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