project_test.py 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095
  1. import copy
  2. import json
  3. import os
  4. import random
  5. import shutil
  6. import tempfile
  7. import pytest
  8. from docker.errors import APIError
  9. from docker.errors import NotFound
  10. from .. import mock
  11. from ..helpers import build_config as load_config
  12. from ..helpers import BUSYBOX_IMAGE_WITH_TAG
  13. from ..helpers import cd
  14. from ..helpers import create_host_file
  15. from .testcases import DockerClientTestCase
  16. from .testcases import SWARM_SKIP_CONTAINERS_ALL
  17. from compose.config import config
  18. from compose.config import ConfigurationError
  19. from compose.config import types
  20. from compose.config.types import VolumeFromSpec
  21. from compose.config.types import VolumeSpec
  22. from compose.const import COMPOSE_SPEC as VERSION
  23. from compose.const import LABEL_PROJECT
  24. from compose.const import LABEL_SERVICE
  25. from compose.container import Container
  26. from compose.errors import CompletedUnsuccessfully
  27. from compose.errors import HealthCheckFailed
  28. from compose.errors import NoHealthCheckConfigured
  29. from compose.project import Project
  30. from compose.project import ProjectError
  31. from compose.service import ConvergenceStrategy
  32. from tests.integration.testcases import if_runtime_available
  33. from tests.integration.testcases import is_cluster
  34. from tests.integration.testcases import no_cluster
  35. def build_config(**kwargs):
  36. return config.Config(
  37. config_version=kwargs.get('version', VERSION),
  38. version=kwargs.get('version', VERSION),
  39. services=kwargs.get('services'),
  40. volumes=kwargs.get('volumes'),
  41. networks=kwargs.get('networks'),
  42. secrets=kwargs.get('secrets'),
  43. configs=kwargs.get('configs'),
  44. )
  45. class ProjectTest(DockerClientTestCase):
  46. def test_containers(self):
  47. web = self.create_service('web')
  48. db = self.create_service('db')
  49. project = Project('composetest', [web, db], self.client)
  50. project.up()
  51. containers = project.containers()
  52. assert len(containers) == 2
  53. @pytest.mark.skipif(SWARM_SKIP_CONTAINERS_ALL, reason='Swarm /containers/json bug')
  54. def test_containers_stopped(self):
  55. web = self.create_service('web')
  56. db = self.create_service('db')
  57. project = Project('composetest', [web, db], self.client)
  58. project.up()
  59. assert len(project.containers()) == 2
  60. assert len(project.containers(stopped=True)) == 2
  61. project.stop()
  62. assert len(project.containers()) == 0
  63. assert len(project.containers(stopped=True)) == 2
  64. def test_containers_with_service_names(self):
  65. web = self.create_service('web')
  66. db = self.create_service('db')
  67. project = Project('composetest', [web, db], self.client)
  68. project.up()
  69. containers = project.containers(['web'])
  70. assert len(containers) == 1
  71. assert containers[0].name.startswith('composetest_web_')
  72. def test_containers_with_extra_service(self):
  73. web = self.create_service('web')
  74. web_1 = web.create_container()
  75. db = self.create_service('db')
  76. db_1 = db.create_container()
  77. self.create_service('extra').create_container()
  78. project = Project('composetest', [web, db], self.client)
  79. assert set(project.containers(stopped=True)) == {web_1, db_1}
  80. def test_parallel_pull_with_no_image(self):
  81. config_data = build_config(
  82. services=[{
  83. 'name': 'web',
  84. 'build': {'context': '.'},
  85. }],
  86. )
  87. project = Project.from_config(
  88. name='composetest',
  89. config_data=config_data,
  90. client=self.client
  91. )
  92. project.pull(parallel_pull=True)
  93. def test_volumes_from_service(self):
  94. project = Project.from_config(
  95. name='composetest',
  96. config_data=load_config({
  97. 'data': {
  98. 'image': BUSYBOX_IMAGE_WITH_TAG,
  99. 'volumes': ['/var/data'],
  100. },
  101. 'db': {
  102. 'image': BUSYBOX_IMAGE_WITH_TAG,
  103. 'volumes_from': ['data'],
  104. },
  105. }),
  106. client=self.client,
  107. )
  108. db = project.get_service('db')
  109. data = project.get_service('data')
  110. assert db.volumes_from == [VolumeFromSpec(data, 'rw', 'service')]
  111. def test_volumes_from_container(self):
  112. data_container = Container.create(
  113. self.client,
  114. image=BUSYBOX_IMAGE_WITH_TAG,
  115. volumes=['/var/data'],
  116. name='composetest_data_container',
  117. labels={LABEL_PROJECT: 'composetest'},
  118. host_config={},
  119. )
  120. project = Project.from_config(
  121. name='composetest',
  122. config_data=load_config({
  123. 'db': {
  124. 'image': BUSYBOX_IMAGE_WITH_TAG,
  125. 'volumes_from': ['composetest_data_container'],
  126. },
  127. }),
  128. client=self.client,
  129. )
  130. db = project.get_service('db')
  131. assert db._get_volumes_from() == [data_container.id + ':rw']
  132. @no_cluster('container networks not supported in Swarm')
  133. def test_network_mode_from_service(self):
  134. project = Project.from_config(
  135. name='composetest',
  136. client=self.client,
  137. config_data=load_config({
  138. 'services': {
  139. 'net': {
  140. 'image': BUSYBOX_IMAGE_WITH_TAG,
  141. 'command': ["top"]
  142. },
  143. 'web': {
  144. 'image': BUSYBOX_IMAGE_WITH_TAG,
  145. 'network_mode': 'service:net',
  146. 'command': ["top"]
  147. },
  148. },
  149. }),
  150. )
  151. project.up()
  152. web = project.get_service('web')
  153. net = project.get_service('net')
  154. assert web.network_mode.mode == 'container:' + net.containers()[0].id
  155. @no_cluster('container networks not supported in Swarm')
  156. def test_network_mode_from_container(self):
  157. def get_project():
  158. return Project.from_config(
  159. name='composetest',
  160. config_data=load_config({
  161. 'services': {
  162. 'web': {
  163. 'image': BUSYBOX_IMAGE_WITH_TAG,
  164. 'network_mode': 'container:composetest_net_container'
  165. },
  166. },
  167. }),
  168. client=self.client,
  169. )
  170. with pytest.raises(ConfigurationError) as excinfo:
  171. get_project()
  172. assert "container 'composetest_net_container' which does not exist" in excinfo.exconly()
  173. net_container = Container.create(
  174. self.client,
  175. image=BUSYBOX_IMAGE_WITH_TAG,
  176. name='composetest_net_container',
  177. command='top',
  178. labels={LABEL_PROJECT: 'composetest'},
  179. host_config={},
  180. )
  181. net_container.start()
  182. project = get_project()
  183. project.up()
  184. web = project.get_service('web')
  185. assert web.network_mode.mode == 'container:' + net_container.id
  186. @no_cluster('container networks not supported in Swarm')
  187. def test_net_from_service_v1(self):
  188. project = Project.from_config(
  189. name='composetest',
  190. config_data=load_config({
  191. 'net': {
  192. 'image': BUSYBOX_IMAGE_WITH_TAG,
  193. 'command': ["top"]
  194. },
  195. 'web': {
  196. 'image': BUSYBOX_IMAGE_WITH_TAG,
  197. 'net': 'container:net',
  198. 'command': ["top"]
  199. },
  200. }),
  201. client=self.client,
  202. )
  203. project.up()
  204. web = project.get_service('web')
  205. net = project.get_service('net')
  206. assert web.network_mode.mode == 'container:' + net.containers()[0].id
  207. @no_cluster('container networks not supported in Swarm')
  208. def test_net_from_container_v1(self):
  209. def get_project():
  210. return Project.from_config(
  211. name='composetest',
  212. config_data=load_config({
  213. 'web': {
  214. 'image': BUSYBOX_IMAGE_WITH_TAG,
  215. 'net': 'container:composetest_net_container'
  216. },
  217. }),
  218. client=self.client,
  219. )
  220. with pytest.raises(ConfigurationError) as excinfo:
  221. get_project()
  222. assert "container 'composetest_net_container' which does not exist" in excinfo.exconly()
  223. net_container = Container.create(
  224. self.client,
  225. image=BUSYBOX_IMAGE_WITH_TAG,
  226. name='composetest_net_container',
  227. command='top',
  228. labels={LABEL_PROJECT: 'composetest'},
  229. host_config={},
  230. )
  231. net_container.start()
  232. project = get_project()
  233. project.up()
  234. web = project.get_service('web')
  235. assert web.network_mode.mode == 'container:' + net_container.id
  236. def test_start_pause_unpause_stop_kill_remove(self):
  237. web = self.create_service('web')
  238. db = self.create_service('db')
  239. project = Project('composetest', [web, db], self.client)
  240. project.start()
  241. assert len(web.containers()) == 0
  242. assert len(db.containers()) == 0
  243. web_container_1 = web.create_container()
  244. web_container_2 = web.create_container()
  245. db_container = db.create_container()
  246. project.start(service_names=['web'])
  247. assert {c.name for c in project.containers() if c.is_running} == {
  248. web_container_1.name, web_container_2.name}
  249. project.start()
  250. assert {c.name for c in project.containers() if c.is_running} == {
  251. web_container_1.name, web_container_2.name, db_container.name}
  252. project.pause(service_names=['web'])
  253. assert {c.name for c in project.containers() if c.is_paused} == {
  254. web_container_1.name, web_container_2.name}
  255. project.pause()
  256. assert {c.name for c in project.containers() if c.is_paused} == {
  257. web_container_1.name, web_container_2.name, db_container.name}
  258. project.unpause(service_names=['db'])
  259. assert len([c.name for c in project.containers() if c.is_paused]) == 2
  260. project.unpause()
  261. assert len([c.name for c in project.containers() if c.is_paused]) == 0
  262. project.stop(service_names=['web'], timeout=1)
  263. assert {c.name for c in project.containers() if c.is_running} == {db_container.name}
  264. project.kill(service_names=['db'])
  265. assert len([c for c in project.containers() if c.is_running]) == 0
  266. assert len(project.containers(stopped=True)) == 3
  267. project.remove_stopped(service_names=['web'])
  268. assert len(project.containers(stopped=True)) == 1
  269. project.remove_stopped()
  270. assert len(project.containers(stopped=True)) == 0
  271. def test_create(self):
  272. web = self.create_service('web')
  273. db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])
  274. project = Project('composetest', [web, db], self.client)
  275. project.create(['db'])
  276. containers = project.containers(stopped=True)
  277. assert len(containers) == 1
  278. assert not containers[0].is_running
  279. db_containers = db.containers(stopped=True)
  280. assert len(db_containers) == 1
  281. assert not db_containers[0].is_running
  282. assert len(web.containers(stopped=True)) == 0
  283. def test_create_twice(self):
  284. web = self.create_service('web')
  285. db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])
  286. project = Project('composetest', [web, db], self.client)
  287. project.create(['db', 'web'])
  288. project.create(['db', 'web'])
  289. containers = project.containers(stopped=True)
  290. assert len(containers) == 2
  291. db_containers = db.containers(stopped=True)
  292. assert len(db_containers) == 1
  293. assert not db_containers[0].is_running
  294. web_containers = web.containers(stopped=True)
  295. assert len(web_containers) == 1
  296. assert not web_containers[0].is_running
  297. def test_create_with_links(self):
  298. db = self.create_service('db')
  299. web = self.create_service('web', links=[(db, 'db')])
  300. project = Project('composetest', [db, web], self.client)
  301. project.create(['web'])
  302. # self.assertEqual(len(project.containers()), 0)
  303. assert len(project.containers(stopped=True)) == 2
  304. assert not [c for c in project.containers(stopped=True) if c.is_running]
  305. assert len(db.containers(stopped=True)) == 1
  306. assert len(web.containers(stopped=True)) == 1
  307. def test_create_strategy_always(self):
  308. db = self.create_service('db')
  309. project = Project('composetest', [db], self.client)
  310. project.create(['db'])
  311. old_id = project.containers(stopped=True)[0].id
  312. project.create(['db'], strategy=ConvergenceStrategy.always)
  313. assert len(project.containers(stopped=True)) == 1
  314. db_container = project.containers(stopped=True)[0]
  315. assert not db_container.is_running
  316. assert db_container.id != old_id
  317. def test_create_strategy_never(self):
  318. db = self.create_service('db')
  319. project = Project('composetest', [db], self.client)
  320. project.create(['db'])
  321. old_id = project.containers(stopped=True)[0].id
  322. project.create(['db'], strategy=ConvergenceStrategy.never)
  323. assert len(project.containers(stopped=True)) == 1
  324. db_container = project.containers(stopped=True)[0]
  325. assert not db_container.is_running
  326. assert db_container.id == old_id
  327. def test_project_up(self):
  328. web = self.create_service('web')
  329. db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])
  330. project = Project('composetest', [web, db], self.client)
  331. project.start()
  332. assert len(project.containers()) == 0
  333. project.up(['db'])
  334. assert len(project.containers()) == 1
  335. assert len(db.containers()) == 1
  336. assert len(web.containers()) == 0
  337. def test_project_up_starts_uncreated_services(self):
  338. db = self.create_service('db')
  339. web = self.create_service('web', links=[(db, 'db')])
  340. project = Project('composetest', [db, web], self.client)
  341. project.up(['db'])
  342. assert len(project.containers()) == 1
  343. project.up()
  344. assert len(project.containers()) == 2
  345. assert len(db.containers()) == 1
  346. assert len(web.containers()) == 1
  347. def test_recreate_preserves_volumes(self):
  348. web = self.create_service('web')
  349. db = self.create_service('db', volumes=[VolumeSpec.parse('/etc')])
  350. project = Project('composetest', [web, db], self.client)
  351. project.start()
  352. assert len(project.containers()) == 0
  353. project.up(['db'])
  354. assert len(project.containers()) == 1
  355. old_db_id = project.containers()[0].id
  356. db_volume_path = project.containers()[0].get('Volumes./etc')
  357. project.up(strategy=ConvergenceStrategy.always)
  358. assert len(project.containers()) == 2
  359. db_container = [c for c in project.containers() if c.service == 'db'][0]
  360. assert db_container.id != old_db_id
  361. assert db_container.get('Volumes./etc') == db_volume_path
  362. def test_recreate_preserves_mounts(self):
  363. web = self.create_service('web')
  364. db = self.create_service('db', volumes=[types.MountSpec(type='volume', target='/etc')])
  365. project = Project('composetest', [web, db], self.client)
  366. project.start()
  367. assert len(project.containers()) == 0
  368. project.up(['db'])
  369. assert len(project.containers()) == 1
  370. old_db_id = project.containers()[0].id
  371. db_volume_path = project.containers()[0].get_mount('/etc')['Source']
  372. project.up(strategy=ConvergenceStrategy.always)
  373. assert len(project.containers()) == 2
  374. db_container = [c for c in project.containers() if c.service == 'db'][0]
  375. assert db_container.id != old_db_id
  376. assert db_container.get_mount('/etc')['Source'] == db_volume_path
  377. def test_project_up_with_no_recreate_running(self):
  378. web = self.create_service('web')
  379. db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])
  380. project = Project('composetest', [web, db], self.client)
  381. project.start()
  382. assert len(project.containers()) == 0
  383. project.up(['db'])
  384. assert len(project.containers()) == 1
  385. container, = project.containers()
  386. old_db_id = container.id
  387. db_volume_path = container.get_mount('/var/db')['Source']
  388. project.up(strategy=ConvergenceStrategy.never)
  389. assert len(project.containers()) == 2
  390. db_container = [c for c in project.containers() if c.name == container.name][0]
  391. assert db_container.id == old_db_id
  392. assert db_container.get_mount('/var/db')['Source'] == db_volume_path
  393. def test_project_up_with_no_recreate_stopped(self):
  394. web = self.create_service('web')
  395. db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])
  396. project = Project('composetest', [web, db], self.client)
  397. project.start()
  398. assert len(project.containers()) == 0
  399. project.up(['db'])
  400. project.kill()
  401. old_containers = project.containers(stopped=True)
  402. assert len(old_containers) == 1
  403. old_container, = old_containers
  404. old_db_id = old_container.id
  405. db_volume_path = old_container.get_mount('/var/db')['Source']
  406. project.up(strategy=ConvergenceStrategy.never)
  407. new_containers = project.containers(stopped=True)
  408. assert len(new_containers) == 2
  409. assert [c.is_running for c in new_containers] == [True, True]
  410. db_container = [c for c in new_containers if c.service == 'db'][0]
  411. assert db_container.id == old_db_id
  412. assert db_container.get_mount('/var/db')['Source'] == db_volume_path
  413. def test_project_up_without_all_services(self):
  414. console = self.create_service('console')
  415. db = self.create_service('db')
  416. project = Project('composetest', [console, db], self.client)
  417. project.start()
  418. assert len(project.containers()) == 0
  419. project.up()
  420. assert len(project.containers()) == 2
  421. assert len(db.containers()) == 1
  422. assert len(console.containers()) == 1
  423. def test_project_up_starts_links(self):
  424. console = self.create_service('console')
  425. db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])
  426. web = self.create_service('web', links=[(db, 'db')])
  427. project = Project('composetest', [web, db, console], self.client)
  428. project.start()
  429. assert len(project.containers()) == 0
  430. project.up(['web'])
  431. assert len(project.containers()) == 2
  432. assert len(web.containers()) == 1
  433. assert len(db.containers()) == 1
  434. assert len(console.containers()) == 0
  435. def test_project_up_starts_depends(self):
  436. project = Project.from_config(
  437. name='composetest',
  438. config_data=load_config({
  439. 'console': {
  440. 'image': BUSYBOX_IMAGE_WITH_TAG,
  441. 'command': ["top"],
  442. },
  443. 'data': {
  444. 'image': BUSYBOX_IMAGE_WITH_TAG,
  445. 'command': ["top"]
  446. },
  447. 'db': {
  448. 'image': BUSYBOX_IMAGE_WITH_TAG,
  449. 'command': ["top"],
  450. 'volumes_from': ['data'],
  451. },
  452. 'web': {
  453. 'image': BUSYBOX_IMAGE_WITH_TAG,
  454. 'command': ["top"],
  455. 'links': ['db'],
  456. },
  457. }),
  458. client=self.client,
  459. )
  460. project.start()
  461. assert len(project.containers()) == 0
  462. project.up(['web'])
  463. assert len(project.containers()) == 3
  464. assert len(project.get_service('web').containers()) == 1
  465. assert len(project.get_service('db').containers()) == 1
  466. assert len(project.get_service('data').containers()) == 1
  467. assert len(project.get_service('console').containers()) == 0
  468. def test_project_up_with_no_deps(self):
  469. project = Project.from_config(
  470. name='composetest',
  471. config_data=load_config({
  472. 'console': {
  473. 'image': BUSYBOX_IMAGE_WITH_TAG,
  474. 'command': ["top"],
  475. },
  476. 'data': {
  477. 'image': BUSYBOX_IMAGE_WITH_TAG,
  478. 'command': ["top"]
  479. },
  480. 'db': {
  481. 'image': BUSYBOX_IMAGE_WITH_TAG,
  482. 'command': ["top"],
  483. 'volumes_from': ['data'],
  484. },
  485. 'web': {
  486. 'image': BUSYBOX_IMAGE_WITH_TAG,
  487. 'command': ["top"],
  488. 'links': ['db'],
  489. },
  490. }),
  491. client=self.client,
  492. )
  493. project.start()
  494. assert len(project.containers()) == 0
  495. project.up(['db'], start_deps=False)
  496. assert len(project.containers(stopped=True)) == 2
  497. assert len(project.get_service('web').containers()) == 0
  498. assert len(project.get_service('db').containers()) == 1
  499. assert len(project.get_service('data').containers(stopped=True)) == 1
  500. assert not project.get_service('data').containers(stopped=True)[0].is_running
  501. assert len(project.get_service('console').containers()) == 0
  502. def test_project_up_recreate_with_tmpfs_volume(self):
  503. # https://github.com/docker/compose/issues/4751
  504. project = Project.from_config(
  505. name='composetest',
  506. config_data=load_config({
  507. 'version': '2.1',
  508. 'services': {
  509. 'foo': {
  510. 'image': BUSYBOX_IMAGE_WITH_TAG,
  511. 'tmpfs': ['/dev/shm'],
  512. 'volumes': ['/dev/shm']
  513. }
  514. }
  515. }), client=self.client
  516. )
  517. project.up()
  518. project.up(strategy=ConvergenceStrategy.always)
  519. def test_unscale_after_restart(self):
  520. web = self.create_service('web')
  521. project = Project('composetest', [web], self.client)
  522. project.start()
  523. service = project.get_service('web')
  524. service.scale(1)
  525. assert len(service.containers()) == 1
  526. service.scale(3)
  527. assert len(service.containers()) == 3
  528. project.up()
  529. service = project.get_service('web')
  530. assert len(service.containers()) == 1
  531. service.scale(1)
  532. assert len(service.containers()) == 1
  533. project.up(scale_override={'web': 3})
  534. service = project.get_service('web')
  535. assert len(service.containers()) == 3
  536. # does scale=0 ,makes any sense? after recreating at least 1 container is running
  537. service.scale(0)
  538. project.up()
  539. service = project.get_service('web')
  540. assert len(service.containers()) == 1
  541. def test_project_up_networks(self):
  542. config_data = build_config(
  543. services=[{
  544. 'name': 'web',
  545. 'image': BUSYBOX_IMAGE_WITH_TAG,
  546. 'command': 'top',
  547. 'networks': {
  548. 'foo': None,
  549. 'bar': None,
  550. 'baz': {'aliases': ['extra']},
  551. },
  552. }],
  553. networks={
  554. 'foo': {'driver': 'bridge'},
  555. 'bar': {'driver': None},
  556. 'baz': {},
  557. },
  558. )
  559. project = Project.from_config(
  560. client=self.client,
  561. name='composetest',
  562. config_data=config_data,
  563. )
  564. project.up()
  565. containers = project.containers()
  566. assert len(containers) == 1
  567. container, = containers
  568. for net_name in ['foo', 'bar', 'baz']:
  569. full_net_name = 'composetest_{}'.format(net_name)
  570. network_data = self.client.inspect_network(full_net_name)
  571. assert network_data['Name'] == full_net_name
  572. aliases_key = 'NetworkSettings.Networks.{net}.Aliases'
  573. assert 'web' in container.get(aliases_key.format(net='composetest_foo'))
  574. assert 'web' in container.get(aliases_key.format(net='composetest_baz'))
  575. assert 'extra' in container.get(aliases_key.format(net='composetest_baz'))
  576. foo_data = self.client.inspect_network('composetest_foo')
  577. assert foo_data['Driver'] == 'bridge'
  578. def test_up_with_ipam_config(self):
  579. config_data = build_config(
  580. services=[{
  581. 'name': 'web',
  582. 'image': BUSYBOX_IMAGE_WITH_TAG,
  583. 'networks': {'front': None},
  584. }],
  585. networks={
  586. 'front': {
  587. 'driver': 'bridge',
  588. 'driver_opts': {
  589. "com.docker.network.bridge.enable_icc": "false",
  590. },
  591. 'ipam': {
  592. 'driver': 'default',
  593. 'config': [{
  594. "subnet": "172.28.0.0/16",
  595. "ip_range": "172.28.5.0/24",
  596. "gateway": "172.28.5.254",
  597. "aux_addresses": {
  598. "a": "172.28.1.5",
  599. "b": "172.28.1.6",
  600. "c": "172.28.1.7",
  601. },
  602. }],
  603. },
  604. },
  605. },
  606. )
  607. project = Project.from_config(
  608. client=self.client,
  609. name='composetest',
  610. config_data=config_data,
  611. )
  612. project.up()
  613. network = self.client.networks(names=['composetest_front'])[0]
  614. assert network['Options'] == {
  615. "com.docker.network.bridge.enable_icc": "false"
  616. }
  617. assert network['IPAM'] == {
  618. 'Driver': 'default',
  619. 'Options': None,
  620. 'Config': [{
  621. 'Subnet': "172.28.0.0/16",
  622. 'IPRange': "172.28.5.0/24",
  623. 'Gateway': "172.28.5.254",
  624. 'AuxiliaryAddresses': {
  625. 'a': '172.28.1.5',
  626. 'b': '172.28.1.6',
  627. 'c': '172.28.1.7',
  628. },
  629. }],
  630. }
  631. def test_up_with_ipam_options(self):
  632. config_data = build_config(
  633. services=[{
  634. 'name': 'web',
  635. 'image': BUSYBOX_IMAGE_WITH_TAG,
  636. 'networks': {'front': None},
  637. }],
  638. networks={
  639. 'front': {
  640. 'driver': 'bridge',
  641. 'ipam': {
  642. 'driver': 'default',
  643. 'options': {
  644. "com.docker.compose.network.test": "9-29-045"
  645. }
  646. },
  647. },
  648. },
  649. )
  650. project = Project.from_config(
  651. client=self.client,
  652. name='composetest',
  653. config_data=config_data,
  654. )
  655. project.up()
  656. network = self.client.networks(names=['composetest_front'])[0]
  657. assert network['IPAM']['Options'] == {
  658. "com.docker.compose.network.test": "9-29-045"
  659. }
  660. def test_up_with_network_static_addresses(self):
  661. config_data = build_config(
  662. services=[{
  663. 'name': 'web',
  664. 'image': BUSYBOX_IMAGE_WITH_TAG,
  665. 'command': 'top',
  666. 'networks': {
  667. 'static_test': {
  668. 'ipv4_address': '172.16.100.100',
  669. 'ipv6_address': 'fe80::1001:102'
  670. }
  671. },
  672. }],
  673. networks={
  674. 'static_test': {
  675. 'driver': 'bridge',
  676. 'driver_opts': {
  677. "com.docker.network.enable_ipv6": "true",
  678. },
  679. 'ipam': {
  680. 'driver': 'default',
  681. 'config': [
  682. {"subnet": "172.16.100.0/24",
  683. "gateway": "172.16.100.1"},
  684. {"subnet": "fe80::/64",
  685. "gateway": "fe80::1001:1"}
  686. ]
  687. },
  688. 'enable_ipv6': True,
  689. }
  690. }
  691. )
  692. project = Project.from_config(
  693. client=self.client,
  694. name='composetest',
  695. config_data=config_data,
  696. )
  697. project.up(detached=True)
  698. service_container = project.get_service('web').containers()[0]
  699. ipam_config = (service_container.inspect().get('NetworkSettings', {}).
  700. get('Networks', {}).get('composetest_static_test', {}).
  701. get('IPAMConfig', {}))
  702. assert ipam_config.get('IPv4Address') == '172.16.100.100'
  703. assert ipam_config.get('IPv6Address') == 'fe80::1001:102'
  704. def test_up_with_network_priorities(self):
  705. mac_address = '74:6f:75:68:6f:75'
  706. def get_config_data(p1, p2, p3):
  707. return build_config(
  708. services=[{
  709. 'name': 'web',
  710. 'image': BUSYBOX_IMAGE_WITH_TAG,
  711. 'networks': {
  712. 'n1': {
  713. 'priority': p1,
  714. },
  715. 'n2': {
  716. 'priority': p2,
  717. },
  718. 'n3': {
  719. 'priority': p3,
  720. }
  721. },
  722. 'command': 'top',
  723. 'mac_address': mac_address
  724. }],
  725. networks={
  726. 'n1': {},
  727. 'n2': {},
  728. 'n3': {}
  729. }
  730. )
  731. config1 = get_config_data(1000, 1, 1)
  732. config2 = get_config_data(2, 3, 1)
  733. config3 = get_config_data(5, 40, 100)
  734. project = Project.from_config(
  735. client=self.client,
  736. name='composetest',
  737. config_data=config1
  738. )
  739. project.up(detached=True)
  740. service_container = project.get_service('web').containers()[0]
  741. net_config = service_container.inspect()['NetworkSettings']['Networks']['composetest_n1']
  742. assert net_config['MacAddress'] == mac_address
  743. project = Project.from_config(
  744. client=self.client,
  745. name='composetest',
  746. config_data=config2
  747. )
  748. project.up(detached=True)
  749. service_container = project.get_service('web').containers()[0]
  750. net_config = service_container.inspect()['NetworkSettings']['Networks']['composetest_n2']
  751. assert net_config['MacAddress'] == mac_address
  752. project = Project.from_config(
  753. client=self.client,
  754. name='composetest',
  755. config_data=config3
  756. )
  757. project.up(detached=True)
  758. service_container = project.get_service('web').containers()[0]
  759. net_config = service_container.inspect()['NetworkSettings']['Networks']['composetest_n3']
  760. assert net_config['MacAddress'] == mac_address
  761. def test_up_with_enable_ipv6(self):
  762. self.require_api_version('1.23')
  763. config_data = build_config(
  764. services=[{
  765. 'name': 'web',
  766. 'image': BUSYBOX_IMAGE_WITH_TAG,
  767. 'command': 'top',
  768. 'networks': {
  769. 'static_test': {
  770. 'ipv6_address': 'fe80::1001:102'
  771. }
  772. },
  773. }],
  774. networks={
  775. 'static_test': {
  776. 'driver': 'bridge',
  777. 'enable_ipv6': True,
  778. 'ipam': {
  779. 'driver': 'default',
  780. 'config': [
  781. {"subnet": "fe80::/64",
  782. "gateway": "fe80::1001:1"}
  783. ]
  784. }
  785. }
  786. }
  787. )
  788. project = Project.from_config(
  789. client=self.client,
  790. name='composetest',
  791. config_data=config_data,
  792. )
  793. project.up(detached=True)
  794. network = [n for n in self.client.networks() if 'static_test' in n['Name']][0]
  795. service_container = project.get_service('web').containers()[0]
  796. assert network['EnableIPv6'] is True
  797. ipam_config = (service_container.inspect().get('NetworkSettings', {}).
  798. get('Networks', {}).get('composetest_static_test', {}).
  799. get('IPAMConfig', {}))
  800. assert ipam_config.get('IPv6Address') == 'fe80::1001:102'
  801. def test_up_with_network_static_addresses_missing_subnet(self):
  802. config_data = build_config(
  803. services=[{
  804. 'name': 'web',
  805. 'image': BUSYBOX_IMAGE_WITH_TAG,
  806. 'networks': {
  807. 'static_test': {
  808. 'ipv4_address': '172.16.100.100',
  809. 'ipv6_address': 'fe80::1001:101'
  810. }
  811. },
  812. }],
  813. networks={
  814. 'static_test': {
  815. 'driver': 'bridge',
  816. 'driver_opts': {
  817. "com.docker.network.enable_ipv6": "true",
  818. },
  819. 'ipam': {
  820. 'driver': 'default',
  821. },
  822. },
  823. },
  824. )
  825. project = Project.from_config(
  826. client=self.client,
  827. name='composetest',
  828. config_data=config_data,
  829. )
  830. with pytest.raises(ProjectError):
  831. project.up()
  832. def test_up_with_network_link_local_ips(self):
  833. config_data = build_config(
  834. services=[{
  835. 'name': 'web',
  836. 'image': BUSYBOX_IMAGE_WITH_TAG,
  837. 'networks': {
  838. 'linklocaltest': {
  839. 'link_local_ips': ['169.254.8.8']
  840. }
  841. }
  842. }],
  843. networks={
  844. 'linklocaltest': {'driver': 'bridge'}
  845. }
  846. )
  847. project = Project.from_config(
  848. client=self.client,
  849. name='composetest',
  850. config_data=config_data
  851. )
  852. project.up(detached=True)
  853. service_container = project.get_service('web').containers(stopped=True)[0]
  854. ipam_config = service_container.inspect().get(
  855. 'NetworkSettings', {}
  856. ).get(
  857. 'Networks', {}
  858. ).get(
  859. 'composetest_linklocaltest', {}
  860. ).get('IPAMConfig', {})
  861. assert 'LinkLocalIPs' in ipam_config
  862. assert ipam_config['LinkLocalIPs'] == ['169.254.8.8']
  863. def test_up_with_custom_name_resources(self):
  864. config_data = build_config(
  865. services=[{
  866. 'name': 'web',
  867. 'volumes': [VolumeSpec.parse('foo:/container-path')],
  868. 'networks': {'foo': {}},
  869. 'image': BUSYBOX_IMAGE_WITH_TAG
  870. }],
  871. networks={
  872. 'foo': {
  873. 'name': 'zztop',
  874. 'labels': {'com.docker.compose.test_value': 'sharpdressedman'}
  875. }
  876. },
  877. volumes={
  878. 'foo': {
  879. 'name': 'acdc',
  880. 'labels': {'com.docker.compose.test_value': 'thefuror'}
  881. }
  882. }
  883. )
  884. project = Project.from_config(
  885. client=self.client,
  886. name='composetest',
  887. config_data=config_data
  888. )
  889. project.up(detached=True)
  890. network = [n for n in self.client.networks() if n['Name'] == 'zztop'][0]
  891. volume = [v for v in self.client.volumes()['Volumes'] if v['Name'] == 'acdc'][0]
  892. assert network['Labels']['com.docker.compose.test_value'] == 'sharpdressedman'
  893. assert volume['Labels']['com.docker.compose.test_value'] == 'thefuror'
  894. def test_up_with_isolation(self):
  895. self.require_api_version('1.24')
  896. config_data = build_config(
  897. services=[{
  898. 'name': 'web',
  899. 'image': BUSYBOX_IMAGE_WITH_TAG,
  900. 'isolation': 'default'
  901. }],
  902. )
  903. project = Project.from_config(
  904. client=self.client,
  905. name='composetest',
  906. config_data=config_data
  907. )
  908. project.up(detached=True)
  909. service_container = project.get_service('web').containers(stopped=True)[0]
  910. assert service_container.inspect()['HostConfig']['Isolation'] == 'default'
  911. def test_up_with_invalid_isolation(self):
  912. self.require_api_version('1.24')
  913. config_data = build_config(
  914. services=[{
  915. 'name': 'web',
  916. 'image': BUSYBOX_IMAGE_WITH_TAG,
  917. 'isolation': 'foobar'
  918. }],
  919. )
  920. project = Project.from_config(
  921. client=self.client,
  922. name='composetest',
  923. config_data=config_data
  924. )
  925. with pytest.raises(ProjectError):
  926. project.up()
  927. @if_runtime_available('runc')
  928. def test_up_with_runtime(self):
  929. self.require_api_version('1.30')
  930. config_data = build_config(
  931. services=[{
  932. 'name': 'web',
  933. 'image': BUSYBOX_IMAGE_WITH_TAG,
  934. 'runtime': 'runc'
  935. }],
  936. )
  937. project = Project.from_config(
  938. client=self.client,
  939. name='composetest',
  940. config_data=config_data
  941. )
  942. project.up(detached=True)
  943. service_container = project.get_service('web').containers(stopped=True)[0]
  944. assert service_container.inspect()['HostConfig']['Runtime'] == 'runc'
  945. def test_up_with_invalid_runtime(self):
  946. self.require_api_version('1.30')
  947. config_data = build_config(
  948. services=[{
  949. 'name': 'web',
  950. 'image': BUSYBOX_IMAGE_WITH_TAG,
  951. 'runtime': 'foobar'
  952. }],
  953. )
  954. project = Project.from_config(
  955. client=self.client,
  956. name='composetest',
  957. config_data=config_data
  958. )
  959. with pytest.raises(ProjectError):
  960. project.up()
  961. @if_runtime_available('nvidia')
  962. def test_up_with_nvidia_runtime(self):
  963. self.require_api_version('1.30')
  964. config_data = build_config(
  965. services=[{
  966. 'name': 'web',
  967. 'image': BUSYBOX_IMAGE_WITH_TAG,
  968. 'runtime': 'nvidia'
  969. }],
  970. )
  971. project = Project.from_config(
  972. client=self.client,
  973. name='composetest',
  974. config_data=config_data
  975. )
  976. project.up(detached=True)
  977. service_container = project.get_service('web').containers(stopped=True)[0]
  978. assert service_container.inspect()['HostConfig']['Runtime'] == 'nvidia'
  979. def test_project_up_with_network_internal(self):
  980. self.require_api_version('1.23')
  981. config_data = build_config(
  982. services=[{
  983. 'name': 'web',
  984. 'image': BUSYBOX_IMAGE_WITH_TAG,
  985. 'networks': {'internal': None},
  986. }],
  987. networks={
  988. 'internal': {'driver': 'bridge', 'internal': True},
  989. },
  990. )
  991. project = Project.from_config(
  992. client=self.client,
  993. name='composetest',
  994. config_data=config_data,
  995. )
  996. project.up()
  997. network = self.client.networks(names=['composetest_internal'])[0]
  998. assert network['Internal'] is True
  999. def test_project_up_with_network_label(self):
  1000. self.require_api_version('1.23')
  1001. network_name = 'network_with_label'
  1002. config_data = build_config(
  1003. services=[{
  1004. 'name': 'web',
  1005. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1006. 'networks': {network_name: None}
  1007. }],
  1008. networks={
  1009. network_name: {'labels': {'label_key': 'label_val'}}
  1010. }
  1011. )
  1012. project = Project.from_config(
  1013. client=self.client,
  1014. name='composetest',
  1015. config_data=config_data
  1016. )
  1017. project.up()
  1018. networks = [
  1019. n for n in self.client.networks()
  1020. if n['Name'].startswith('composetest_')
  1021. ]
  1022. assert [n['Name'] for n in networks] == ['composetest_{}'.format(network_name)]
  1023. assert 'label_key' in networks[0]['Labels']
  1024. assert networks[0]['Labels']['label_key'] == 'label_val'
  1025. def test_project_up_volumes(self):
  1026. vol_name = '{:x}'.format(random.getrandbits(32))
  1027. full_vol_name = 'composetest_{}'.format(vol_name)
  1028. config_data = build_config(
  1029. services=[{
  1030. 'name': 'web',
  1031. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1032. 'command': 'top'
  1033. }],
  1034. volumes={vol_name: {'driver': 'local'}},
  1035. )
  1036. project = Project.from_config(
  1037. name='composetest',
  1038. config_data=config_data, client=self.client
  1039. )
  1040. project.up()
  1041. assert len(project.containers()) == 1
  1042. volume_data = self.get_volume_data(full_vol_name)
  1043. assert volume_data['Name'].split('/')[-1] == full_vol_name
  1044. assert volume_data['Driver'] == 'local'
  1045. def test_project_up_with_volume_labels(self):
  1046. self.require_api_version('1.23')
  1047. volume_name = 'volume_with_label'
  1048. config_data = build_config(
  1049. services=[{
  1050. 'name': 'web',
  1051. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1052. 'volumes': [VolumeSpec.parse('{}:/data'.format(volume_name))]
  1053. }],
  1054. volumes={
  1055. volume_name: {
  1056. 'labels': {
  1057. 'label_key': 'label_val'
  1058. }
  1059. }
  1060. },
  1061. )
  1062. project = Project.from_config(
  1063. client=self.client,
  1064. name='composetest',
  1065. config_data=config_data,
  1066. )
  1067. project.up()
  1068. volumes = [
  1069. v for v in self.client.volumes().get('Volumes', [])
  1070. if v['Name'].split('/')[-1].startswith('composetest_')
  1071. ]
  1072. assert {v['Name'].split('/')[-1] for v in volumes} == {
  1073. 'composetest_{}'.format(volume_name)
  1074. }
  1075. assert 'label_key' in volumes[0]['Labels']
  1076. assert volumes[0]['Labels']['label_key'] == 'label_val'
  1077. def test_project_up_logging_with_multiple_files(self):
  1078. base_file = config.ConfigFile(
  1079. 'base.yml',
  1080. {
  1081. 'services': {
  1082. 'simple': {'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top'},
  1083. 'another': {
  1084. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1085. 'command': 'top',
  1086. 'logging': {
  1087. 'driver': "json-file",
  1088. 'options': {
  1089. 'max-size': "10m"
  1090. }
  1091. }
  1092. }
  1093. }
  1094. })
  1095. override_file = config.ConfigFile(
  1096. 'override.yml',
  1097. {
  1098. 'services': {
  1099. 'another': {
  1100. 'logging': {
  1101. 'driver': "none"
  1102. }
  1103. }
  1104. }
  1105. })
  1106. details = config.ConfigDetails('.', [base_file, override_file])
  1107. tmpdir = tempfile.mkdtemp('logging_test')
  1108. self.addCleanup(shutil.rmtree, tmpdir)
  1109. with cd(tmpdir):
  1110. config_data = config.load(details)
  1111. project = Project.from_config(
  1112. name='composetest', config_data=config_data, client=self.client
  1113. )
  1114. project.up()
  1115. containers = project.containers()
  1116. assert len(containers) == 2
  1117. another = project.get_service('another').containers()[0]
  1118. log_config = another.get('HostConfig.LogConfig')
  1119. assert log_config
  1120. assert log_config.get('Type') == 'none'
  1121. def test_project_up_port_mappings_with_multiple_files(self):
  1122. base_file = config.ConfigFile(
  1123. 'base.yml',
  1124. {
  1125. 'services': {
  1126. 'simple': {
  1127. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1128. 'command': 'top',
  1129. 'ports': ['1234:1234']
  1130. },
  1131. },
  1132. })
  1133. override_file = config.ConfigFile(
  1134. 'override.yml',
  1135. {
  1136. 'services': {
  1137. 'simple': {
  1138. 'ports': ['1234:1234']
  1139. }
  1140. }
  1141. })
  1142. details = config.ConfigDetails('.', [base_file, override_file])
  1143. config_data = config.load(details)
  1144. project = Project.from_config(
  1145. name='composetest', config_data=config_data, client=self.client
  1146. )
  1147. project.up()
  1148. containers = project.containers()
  1149. assert len(containers) == 1
  1150. def test_project_up_config_scale(self):
  1151. config_data = build_config(
  1152. services=[{
  1153. 'name': 'web',
  1154. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1155. 'command': 'top',
  1156. 'scale': 3
  1157. }]
  1158. )
  1159. project = Project.from_config(
  1160. name='composetest', config_data=config_data, client=self.client
  1161. )
  1162. project.up()
  1163. assert len(project.containers()) == 3
  1164. project.up(scale_override={'web': 2})
  1165. assert len(project.containers()) == 2
  1166. project.up(scale_override={'web': 4})
  1167. assert len(project.containers()) == 4
  1168. project.stop()
  1169. project.up()
  1170. assert len(project.containers()) == 3
  1171. def test_project_up_scale_with_stopped_containers(self):
  1172. config_data = build_config(
  1173. services=[{
  1174. 'name': 'web',
  1175. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1176. 'command': 'top',
  1177. 'scale': 2
  1178. }]
  1179. )
  1180. project = Project.from_config(
  1181. name='composetest', config_data=config_data, client=self.client
  1182. )
  1183. project.up()
  1184. containers = project.containers()
  1185. assert len(containers) == 2
  1186. self.client.stop(containers[0].id)
  1187. project.up(scale_override={'web': 2})
  1188. containers = project.containers()
  1189. assert len(containers) == 2
  1190. self.client.stop(containers[0].id)
  1191. project.up(scale_override={'web': 3})
  1192. assert len(project.containers()) == 3
  1193. self.client.stop(containers[0].id)
  1194. project.up(scale_override={'web': 1})
  1195. assert len(project.containers()) == 1
  1196. def test_initialize_volumes(self):
  1197. vol_name = '{:x}'.format(random.getrandbits(32))
  1198. full_vol_name = 'composetest_{}'.format(vol_name)
  1199. config_data = build_config(
  1200. services=[{
  1201. 'name': 'web',
  1202. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1203. 'command': 'top'
  1204. }],
  1205. volumes={vol_name: {}},
  1206. )
  1207. project = Project.from_config(
  1208. name='composetest',
  1209. config_data=config_data, client=self.client
  1210. )
  1211. project.volumes.initialize()
  1212. volume_data = self.get_volume_data(full_vol_name)
  1213. assert volume_data['Name'].split('/')[-1] == full_vol_name
  1214. assert volume_data['Driver'] == 'local'
  1215. def test_project_up_implicit_volume_driver(self):
  1216. vol_name = '{:x}'.format(random.getrandbits(32))
  1217. full_vol_name = 'composetest_{}'.format(vol_name)
  1218. config_data = build_config(
  1219. services=[{
  1220. 'name': 'web',
  1221. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1222. 'command': 'top'
  1223. }],
  1224. volumes={vol_name: {}},
  1225. )
  1226. project = Project.from_config(
  1227. name='composetest',
  1228. config_data=config_data, client=self.client
  1229. )
  1230. project.up()
  1231. volume_data = self.get_volume_data(full_vol_name)
  1232. assert volume_data['Name'].split('/')[-1] == full_vol_name
  1233. assert volume_data['Driver'] == 'local'
  1234. def test_project_up_with_secrets(self):
  1235. node = create_host_file(self.client, os.path.abspath('tests/fixtures/secrets/default'))
  1236. config_data = build_config(
  1237. services=[{
  1238. 'name': 'web',
  1239. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1240. 'command': 'cat /run/secrets/special',
  1241. 'secrets': [
  1242. types.ServiceSecret.parse({'source': 'super', 'target': 'special'}),
  1243. ],
  1244. 'environment': ['constraint:node=={}'.format(node if node is not None else '*')]
  1245. }],
  1246. secrets={
  1247. 'super': {
  1248. 'file': os.path.abspath('tests/fixtures/secrets/default'),
  1249. },
  1250. },
  1251. )
  1252. project = Project.from_config(
  1253. client=self.client,
  1254. name='composetest',
  1255. config_data=config_data,
  1256. )
  1257. project.up()
  1258. project.stop()
  1259. containers = project.containers(stopped=True)
  1260. assert len(containers) == 1
  1261. container, = containers
  1262. output = container.logs()
  1263. assert output == b"This is the secret\n"
  1264. def test_project_up_with_added_secrets(self):
  1265. node = create_host_file(self.client, os.path.abspath('tests/fixtures/secrets/default'))
  1266. config_input1 = {
  1267. 'services': [
  1268. {
  1269. 'name': 'web',
  1270. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1271. 'command': 'cat /run/secrets/special',
  1272. 'environment': ['constraint:node=={}'.format(node if node is not None else '')]
  1273. }
  1274. ],
  1275. 'secrets': {
  1276. 'super': {
  1277. 'file': os.path.abspath('tests/fixtures/secrets/default')
  1278. }
  1279. }
  1280. }
  1281. config_input2 = copy.deepcopy(config_input1)
  1282. # Add the secret
  1283. config_input2['services'][0]['secrets'] = [
  1284. types.ServiceSecret.parse({'source': 'super', 'target': 'special'})
  1285. ]
  1286. config_data1 = build_config(**config_input1)
  1287. config_data2 = build_config(**config_input2)
  1288. # First up with non-secret
  1289. project = Project.from_config(
  1290. client=self.client,
  1291. name='composetest',
  1292. config_data=config_data1,
  1293. )
  1294. project.up()
  1295. # Then up with secret
  1296. project = Project.from_config(
  1297. client=self.client,
  1298. name='composetest',
  1299. config_data=config_data2,
  1300. )
  1301. project.up()
  1302. project.stop()
  1303. containers = project.containers(stopped=True)
  1304. assert len(containers) == 1
  1305. container, = containers
  1306. output = container.logs()
  1307. assert output == b"This is the secret\n"
  1308. def test_initialize_volumes_invalid_volume_driver(self):
  1309. vol_name = '{:x}'.format(random.getrandbits(32))
  1310. config_data = build_config(
  1311. version=VERSION,
  1312. services=[{
  1313. 'name': 'web',
  1314. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1315. 'command': 'top'
  1316. }],
  1317. volumes={vol_name: {'driver': 'foobar'}},
  1318. )
  1319. project = Project.from_config(
  1320. name='composetest',
  1321. config_data=config_data, client=self.client
  1322. )
  1323. with pytest.raises(APIError if is_cluster(self.client) else config.ConfigurationError):
  1324. project.volumes.initialize()
  1325. @no_cluster('inspect volume by name defect on Swarm Classic')
  1326. def test_initialize_volumes_updated_driver(self):
  1327. vol_name = '{:x}'.format(random.getrandbits(32))
  1328. full_vol_name = 'composetest_{}'.format(vol_name)
  1329. config_data = build_config(
  1330. services=[{
  1331. 'name': 'web',
  1332. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1333. 'command': 'top'
  1334. }],
  1335. volumes={vol_name: {'driver': 'local'}},
  1336. )
  1337. project = Project.from_config(
  1338. name='composetest',
  1339. config_data=config_data, client=self.client
  1340. )
  1341. project.volumes.initialize()
  1342. volume_data = self.get_volume_data(full_vol_name)
  1343. assert volume_data['Name'].split('/')[-1] == full_vol_name
  1344. assert volume_data['Driver'] == 'local'
  1345. config_data = config_data._replace(
  1346. volumes={vol_name: {'driver': 'smb'}}
  1347. )
  1348. project = Project.from_config(
  1349. name='composetest',
  1350. config_data=config_data,
  1351. client=self.client
  1352. )
  1353. with pytest.raises(config.ConfigurationError) as e:
  1354. project.volumes.initialize()
  1355. assert 'Configuration for volume {} specifies driver smb'.format(
  1356. vol_name
  1357. ) in str(e.value)
  1358. @no_cluster('inspect volume by name defect on Swarm Classic')
  1359. def test_initialize_volumes_updated_driver_opts(self):
  1360. vol_name = '{:x}'.format(random.getrandbits(32))
  1361. full_vol_name = 'composetest_{}'.format(vol_name)
  1362. tmpdir = tempfile.mkdtemp(prefix='compose_test_')
  1363. self.addCleanup(shutil.rmtree, tmpdir)
  1364. driver_opts = {'o': 'bind', 'device': tmpdir, 'type': 'none'}
  1365. config_data = build_config(
  1366. services=[{
  1367. 'name': 'web',
  1368. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1369. 'command': 'top'
  1370. }],
  1371. volumes={
  1372. vol_name: {
  1373. 'driver': 'local',
  1374. 'driver_opts': driver_opts
  1375. }
  1376. },
  1377. )
  1378. project = Project.from_config(
  1379. name='composetest',
  1380. config_data=config_data, client=self.client
  1381. )
  1382. project.volumes.initialize()
  1383. volume_data = self.get_volume_data(full_vol_name)
  1384. assert volume_data['Name'].split('/')[-1] == full_vol_name
  1385. assert volume_data['Driver'] == 'local'
  1386. assert volume_data['Options'] == driver_opts
  1387. driver_opts['device'] = '/opt/data/localdata'
  1388. project = Project.from_config(
  1389. name='composetest',
  1390. config_data=config_data,
  1391. client=self.client
  1392. )
  1393. with pytest.raises(config.ConfigurationError) as e:
  1394. project.volumes.initialize()
  1395. assert 'Configuration for volume {} specifies "device" driver_opt {}'.format(
  1396. vol_name, driver_opts['device']
  1397. ) in str(e.value)
  1398. def test_initialize_volumes_updated_blank_driver(self):
  1399. vol_name = '{:x}'.format(random.getrandbits(32))
  1400. full_vol_name = 'composetest_{}'.format(vol_name)
  1401. config_data = build_config(
  1402. services=[{
  1403. 'name': 'web',
  1404. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1405. 'command': 'top'
  1406. }],
  1407. volumes={vol_name: {'driver': 'local'}},
  1408. )
  1409. project = Project.from_config(
  1410. name='composetest',
  1411. config_data=config_data, client=self.client
  1412. )
  1413. project.volumes.initialize()
  1414. volume_data = self.get_volume_data(full_vol_name)
  1415. assert volume_data['Name'].split('/')[-1] == full_vol_name
  1416. assert volume_data['Driver'] == 'local'
  1417. config_data = config_data._replace(
  1418. volumes={vol_name: {}}
  1419. )
  1420. project = Project.from_config(
  1421. name='composetest',
  1422. config_data=config_data,
  1423. client=self.client
  1424. )
  1425. project.volumes.initialize()
  1426. volume_data = self.get_volume_data(full_vol_name)
  1427. assert volume_data['Name'].split('/')[-1] == full_vol_name
  1428. assert volume_data['Driver'] == 'local'
  1429. @no_cluster('inspect volume by name defect on Swarm Classic')
  1430. def test_initialize_volumes_external_volumes(self):
  1431. # Use composetest_ prefix so it gets garbage-collected in tearDown()
  1432. vol_name = 'composetest_{:x}'.format(random.getrandbits(32))
  1433. full_vol_name = 'composetest_{}'.format(vol_name)
  1434. self.client.create_volume(vol_name)
  1435. config_data = build_config(
  1436. services=[{
  1437. 'name': 'web',
  1438. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1439. 'command': 'top'
  1440. }],
  1441. volumes={
  1442. vol_name: {'external': True, 'name': vol_name}
  1443. },
  1444. )
  1445. project = Project.from_config(
  1446. name='composetest',
  1447. config_data=config_data, client=self.client
  1448. )
  1449. project.volumes.initialize()
  1450. with pytest.raises(NotFound):
  1451. self.client.inspect_volume(full_vol_name)
  1452. def test_initialize_volumes_inexistent_external_volume(self):
  1453. vol_name = '{:x}'.format(random.getrandbits(32))
  1454. config_data = build_config(
  1455. services=[{
  1456. 'name': 'web',
  1457. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1458. 'command': 'top'
  1459. }],
  1460. volumes={
  1461. vol_name: {'external': True, 'name': vol_name}
  1462. },
  1463. )
  1464. project = Project.from_config(
  1465. name='composetest',
  1466. config_data=config_data, client=self.client
  1467. )
  1468. with pytest.raises(config.ConfigurationError) as e:
  1469. project.volumes.initialize()
  1470. assert 'Volume {} declared as external'.format(
  1471. vol_name
  1472. ) in str(e.value)
  1473. def test_project_up_named_volumes_in_binds(self):
  1474. vol_name = '{:x}'.format(random.getrandbits(32))
  1475. full_vol_name = 'composetest_{}'.format(vol_name)
  1476. base_file = config.ConfigFile(
  1477. 'base.yml',
  1478. {
  1479. 'services': {
  1480. 'simple': {
  1481. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1482. 'command': 'top',
  1483. 'volumes': ['{}:/data'.format(vol_name)]
  1484. },
  1485. },
  1486. 'volumes': {
  1487. vol_name: {'driver': 'local'}
  1488. }
  1489. })
  1490. config_details = config.ConfigDetails('.', [base_file])
  1491. config_data = config.load(config_details)
  1492. project = Project.from_config(
  1493. name='composetest', config_data=config_data, client=self.client
  1494. )
  1495. service = project.services[0]
  1496. assert service.name == 'simple'
  1497. volumes = service.options.get('volumes')
  1498. assert len(volumes) == 1
  1499. assert volumes[0].external == full_vol_name
  1500. project.up()
  1501. engine_volumes = self.client.volumes()['Volumes']
  1502. container = service.get_container()
  1503. assert [mount['Name'] for mount in container.get('Mounts')] == [full_vol_name]
  1504. assert next((v for v in engine_volumes if v['Name'] == vol_name), None) is None
  1505. def test_project_up_orphans(self):
  1506. config_dict = {
  1507. 'service1': {
  1508. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1509. 'command': 'top',
  1510. }
  1511. }
  1512. config_data = load_config(config_dict)
  1513. project = Project.from_config(
  1514. name='composetest', config_data=config_data, client=self.client
  1515. )
  1516. project.up()
  1517. config_dict['service2'] = config_dict['service1']
  1518. del config_dict['service1']
  1519. config_data = load_config(config_dict)
  1520. project = Project.from_config(
  1521. name='composetest', config_data=config_data, client=self.client
  1522. )
  1523. with mock.patch('compose.project.log') as mock_log:
  1524. project.up()
  1525. mock_log.warning.assert_called_once_with(mock.ANY)
  1526. assert len([
  1527. ctnr for ctnr in project._labeled_containers()
  1528. if ctnr.labels.get(LABEL_SERVICE) == 'service1'
  1529. ]) == 1
  1530. project.up(remove_orphans=True)
  1531. assert len([
  1532. ctnr for ctnr in project._labeled_containers()
  1533. if ctnr.labels.get(LABEL_SERVICE) == 'service1'
  1534. ]) == 0
  1535. def test_project_up_ignore_orphans(self):
  1536. config_dict = {
  1537. 'service1': {
  1538. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1539. 'command': 'top',
  1540. }
  1541. }
  1542. config_data = load_config(config_dict)
  1543. project = Project.from_config(
  1544. name='composetest', config_data=config_data, client=self.client
  1545. )
  1546. project.up()
  1547. config_dict['service2'] = config_dict['service1']
  1548. del config_dict['service1']
  1549. config_data = load_config(config_dict)
  1550. project = Project.from_config(
  1551. name='composetest', config_data=config_data, client=self.client
  1552. )
  1553. with mock.patch('compose.project.log') as mock_log:
  1554. project.up(ignore_orphans=True)
  1555. mock_log.warning.assert_not_called()
  1556. def test_project_up_healthy_dependency(self):
  1557. config_dict = {
  1558. 'version': '2.1',
  1559. 'services': {
  1560. 'svc1': {
  1561. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1562. 'command': 'top',
  1563. 'healthcheck': {
  1564. 'test': 'exit 0',
  1565. 'retries': 1,
  1566. 'timeout': '10s',
  1567. 'interval': '1s'
  1568. },
  1569. },
  1570. 'svc2': {
  1571. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1572. 'command': 'top',
  1573. 'depends_on': {
  1574. 'svc1': {'condition': 'service_healthy'},
  1575. }
  1576. }
  1577. }
  1578. }
  1579. config_data = load_config(config_dict)
  1580. project = Project.from_config(
  1581. name='composetest', config_data=config_data, client=self.client
  1582. )
  1583. project.up()
  1584. containers = project.containers()
  1585. assert len(containers) == 2
  1586. svc1 = project.get_service('svc1')
  1587. svc2 = project.get_service('svc2')
  1588. assert 'svc1' in svc2.get_dependency_names()
  1589. assert svc1.is_healthy()
  1590. def test_project_up_unhealthy_dependency(self):
  1591. config_dict = {
  1592. 'version': '2.1',
  1593. 'services': {
  1594. 'svc1': {
  1595. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1596. 'command': 'top',
  1597. 'healthcheck': {
  1598. 'test': 'exit 1',
  1599. 'retries': 1,
  1600. 'timeout': '10s',
  1601. 'interval': '1s'
  1602. },
  1603. },
  1604. 'svc2': {
  1605. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1606. 'command': 'top',
  1607. 'depends_on': {
  1608. 'svc1': {'condition': 'service_healthy'},
  1609. }
  1610. }
  1611. }
  1612. }
  1613. config_data = load_config(config_dict)
  1614. project = Project.from_config(
  1615. name='composetest', config_data=config_data, client=self.client
  1616. )
  1617. with pytest.raises(ProjectError):
  1618. project.up()
  1619. containers = project.containers()
  1620. assert len(containers) == 1
  1621. svc1 = project.get_service('svc1')
  1622. svc2 = project.get_service('svc2')
  1623. assert 'svc1' in svc2.get_dependency_names()
  1624. with pytest.raises(HealthCheckFailed):
  1625. svc1.is_healthy()
  1626. def test_project_up_no_healthcheck_dependency(self):
  1627. config_dict = {
  1628. 'version': '2.1',
  1629. 'services': {
  1630. 'svc1': {
  1631. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1632. 'command': 'top',
  1633. 'healthcheck': {
  1634. 'disable': True
  1635. },
  1636. },
  1637. 'svc2': {
  1638. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1639. 'command': 'top',
  1640. 'depends_on': {
  1641. 'svc1': {'condition': 'service_healthy'},
  1642. }
  1643. }
  1644. }
  1645. }
  1646. config_data = load_config(config_dict)
  1647. project = Project.from_config(
  1648. name='composetest', config_data=config_data, client=self.client
  1649. )
  1650. with pytest.raises(ProjectError):
  1651. project.up()
  1652. containers = project.containers()
  1653. assert len(containers) == 1
  1654. svc1 = project.get_service('svc1')
  1655. svc2 = project.get_service('svc2')
  1656. assert 'svc1' in svc2.get_dependency_names()
  1657. with pytest.raises(NoHealthCheckConfigured):
  1658. svc1.is_healthy()
  1659. def test_project_up_completed_successfully_dependency(self):
  1660. config_dict = {
  1661. 'version': '2.1',
  1662. 'services': {
  1663. 'svc1': {
  1664. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1665. 'command': 'true'
  1666. },
  1667. 'svc2': {
  1668. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1669. 'command': 'top',
  1670. 'depends_on': {
  1671. 'svc1': {'condition': 'service_completed_successfully'},
  1672. }
  1673. }
  1674. }
  1675. }
  1676. config_data = load_config(config_dict)
  1677. project = Project.from_config(
  1678. name='composetest', config_data=config_data, client=self.client
  1679. )
  1680. project.up()
  1681. svc1 = project.get_service('svc1')
  1682. svc2 = project.get_service('svc2')
  1683. assert 'svc1' in svc2.get_dependency_names()
  1684. assert svc2.containers()[0].is_running
  1685. assert len(svc1.containers()) == 0
  1686. assert svc1.is_completed_successfully()
  1687. def test_project_up_completed_unsuccessfully_dependency(self):
  1688. config_dict = {
  1689. 'version': '2.1',
  1690. 'services': {
  1691. 'svc1': {
  1692. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1693. 'command': 'false'
  1694. },
  1695. 'svc2': {
  1696. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1697. 'command': 'top',
  1698. 'depends_on': {
  1699. 'svc1': {'condition': 'service_completed_successfully'},
  1700. }
  1701. }
  1702. }
  1703. }
  1704. config_data = load_config(config_dict)
  1705. project = Project.from_config(
  1706. name='composetest', config_data=config_data, client=self.client
  1707. )
  1708. with pytest.raises(ProjectError):
  1709. project.up()
  1710. svc1 = project.get_service('svc1')
  1711. svc2 = project.get_service('svc2')
  1712. assert 'svc1' in svc2.get_dependency_names()
  1713. assert len(svc2.containers()) == 0
  1714. with pytest.raises(CompletedUnsuccessfully):
  1715. svc1.is_completed_successfully()
  1716. def test_project_up_completed_differently_dependencies(self):
  1717. config_dict = {
  1718. 'version': '2.1',
  1719. 'services': {
  1720. 'svc1': {
  1721. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1722. 'command': 'true'
  1723. },
  1724. 'svc2': {
  1725. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1726. 'command': 'false'
  1727. },
  1728. 'svc3': {
  1729. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1730. 'command': 'top',
  1731. 'depends_on': {
  1732. 'svc1': {'condition': 'service_completed_successfully'},
  1733. 'svc2': {'condition': 'service_completed_successfully'},
  1734. }
  1735. }
  1736. }
  1737. }
  1738. config_data = load_config(config_dict)
  1739. project = Project.from_config(
  1740. name='composetest', config_data=config_data, client=self.client
  1741. )
  1742. with pytest.raises(ProjectError):
  1743. project.up()
  1744. svc1 = project.get_service('svc1')
  1745. svc2 = project.get_service('svc2')
  1746. svc3 = project.get_service('svc3')
  1747. assert ['svc1', 'svc2'] == svc3.get_dependency_names()
  1748. assert svc1.is_completed_successfully()
  1749. assert len(svc3.containers()) == 0
  1750. with pytest.raises(CompletedUnsuccessfully):
  1751. svc2.is_completed_successfully()
  1752. def test_project_up_seccomp_profile(self):
  1753. seccomp_data = {
  1754. 'defaultAction': 'SCMP_ACT_ALLOW',
  1755. 'syscalls': []
  1756. }
  1757. fd, profile_path = tempfile.mkstemp('_seccomp.json')
  1758. self.addCleanup(os.remove, profile_path)
  1759. with os.fdopen(fd, 'w') as f:
  1760. json.dump(seccomp_data, f)
  1761. config_dict = {
  1762. 'version': '2.3',
  1763. 'services': {
  1764. 'svc1': {
  1765. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1766. 'command': 'top',
  1767. 'security_opt': ['seccomp:"{}"'.format(profile_path)]
  1768. }
  1769. }
  1770. }
  1771. config_data = load_config(config_dict)
  1772. project = Project.from_config(name='composetest', config_data=config_data, client=self.client)
  1773. project.up()
  1774. containers = project.containers()
  1775. assert len(containers) == 1
  1776. remote_secopts = containers[0].get('HostConfig.SecurityOpt')
  1777. assert len(remote_secopts) == 1
  1778. assert remote_secopts[0].startswith('seccomp=')
  1779. assert json.loads(remote_secopts[0].lstrip('seccomp=')) == seccomp_data
  1780. @no_cluster('inspect volume by name defect on Swarm Classic')
  1781. def test_project_up_name_starts_with_illegal_char(self):
  1782. config_dict = {
  1783. 'version': '2.3',
  1784. 'services': {
  1785. 'svc1': {
  1786. 'image': BUSYBOX_IMAGE_WITH_TAG,
  1787. 'command': 'ls',
  1788. 'volumes': ['foo:/foo:rw'],
  1789. 'networks': ['bar'],
  1790. },
  1791. },
  1792. 'volumes': {
  1793. 'foo': {},
  1794. },
  1795. 'networks': {
  1796. 'bar': {},
  1797. }
  1798. }
  1799. config_data = load_config(config_dict)
  1800. project = Project.from_config(
  1801. name='_underscoretest', config_data=config_data, client=self.client
  1802. )
  1803. project.up()
  1804. self.addCleanup(project.down, None, True)
  1805. containers = project.containers(stopped=True)
  1806. assert len(containers) == 1
  1807. assert containers[0].name.startswith('underscoretest_svc1_')
  1808. assert containers[0].project == '_underscoretest'
  1809. full_vol_name = 'underscoretest_foo'
  1810. vol_data = self.get_volume_data(full_vol_name)
  1811. assert vol_data
  1812. assert vol_data['Labels'][LABEL_PROJECT] == '_underscoretest'
  1813. full_net_name = '_underscoretest_bar'
  1814. net_data = self.client.inspect_network(full_net_name)
  1815. assert net_data
  1816. assert net_data['Labels'][LABEL_PROJECT] == '_underscoretest'
  1817. project2 = Project.from_config(
  1818. name='-dashtest', config_data=config_data, client=self.client
  1819. )
  1820. project2.up()
  1821. self.addCleanup(project2.down, None, True)
  1822. containers = project2.containers(stopped=True)
  1823. assert len(containers) == 1
  1824. assert containers[0].name.startswith('dashtest_svc1_')
  1825. assert containers[0].project == '-dashtest'
  1826. full_vol_name = 'dashtest_foo'
  1827. vol_data = self.get_volume_data(full_vol_name)
  1828. assert vol_data
  1829. assert vol_data['Labels'][LABEL_PROJECT] == '-dashtest'
  1830. full_net_name = '-dashtest_bar'
  1831. net_data = self.client.inspect_network(full_net_name)
  1832. assert net_data
  1833. assert net_data['Labels'][LABEL_PROJECT] == '-dashtest'