config_test.py 67 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902
  1. # encoding: utf-8
  2. from __future__ import absolute_import
  3. from __future__ import print_function
  4. from __future__ import unicode_literals
  5. import os
  6. import shutil
  7. import tempfile
  8. from operator import itemgetter
  9. import py
  10. import pytest
  11. from compose.config import config
  12. from compose.config.config import resolve_environment
  13. from compose.config.errors import ConfigurationError
  14. from compose.config.types import VolumeSpec
  15. from compose.const import IS_WINDOWS_PLATFORM
  16. from tests import mock
  17. from tests import unittest
  18. DEFAULT_VERSION = 2
  19. V1 = 1
  20. def make_service_dict(name, service_dict, working_dir, filename=None):
  21. """
  22. Test helper function to construct a ServiceExtendsResolver
  23. """
  24. resolver = config.ServiceExtendsResolver(config.ServiceConfig(
  25. working_dir=working_dir,
  26. filename=filename,
  27. name=name,
  28. config=service_dict), version=1)
  29. return config.process_service(resolver.run())
  30. def service_sort(services):
  31. return sorted(services, key=itemgetter('name'))
  32. def build_config_details(contents, working_dir='working_dir', filename='filename.yml'):
  33. return config.ConfigDetails(
  34. working_dir,
  35. [config.ConfigFile(filename, contents)])
  36. class ConfigTest(unittest.TestCase):
  37. def test_load(self):
  38. service_dicts = config.load(
  39. build_config_details(
  40. {
  41. 'foo': {'image': 'busybox'},
  42. 'bar': {'image': 'busybox', 'environment': ['FOO=1']},
  43. },
  44. 'tests/fixtures/extends',
  45. 'common.yml'
  46. )
  47. ).services
  48. self.assertEqual(
  49. service_sort(service_dicts),
  50. service_sort([
  51. {
  52. 'name': 'bar',
  53. 'image': 'busybox',
  54. 'environment': {'FOO': '1'},
  55. },
  56. {
  57. 'name': 'foo',
  58. 'image': 'busybox',
  59. }
  60. ])
  61. )
  62. def test_load_v2(self):
  63. config_data = config.load(
  64. build_config_details({
  65. 'version': 2,
  66. 'services': {
  67. 'foo': {'image': 'busybox'},
  68. 'bar': {'image': 'busybox', 'environment': ['FOO=1']},
  69. },
  70. 'volumes': {
  71. 'hello': {
  72. 'driver': 'default',
  73. 'driver_opts': {'beep': 'boop'}
  74. }
  75. }
  76. }, 'working_dir', 'filename.yml')
  77. )
  78. service_dicts = config_data.services
  79. volume_dict = config_data.volumes
  80. self.assertEqual(
  81. service_sort(service_dicts),
  82. service_sort([
  83. {
  84. 'name': 'bar',
  85. 'image': 'busybox',
  86. 'environment': {'FOO': '1'},
  87. },
  88. {
  89. 'name': 'foo',
  90. 'image': 'busybox',
  91. }
  92. ])
  93. )
  94. self.assertEqual(volume_dict, {
  95. 'hello': {
  96. 'driver': 'default',
  97. 'driver_opts': {'beep': 'boop'}
  98. }
  99. })
  100. def test_load_service_with_name_version(self):
  101. config_data = config.load(
  102. build_config_details({
  103. 'version': {
  104. 'image': 'busybox'
  105. }
  106. }, 'working_dir', 'filename.yml')
  107. )
  108. service_dicts = config_data.services
  109. self.assertEqual(
  110. service_sort(service_dicts),
  111. service_sort([
  112. {
  113. 'name': 'version',
  114. 'image': 'busybox',
  115. }
  116. ])
  117. )
  118. def test_load_invalid_version(self):
  119. with self.assertRaises(ConfigurationError):
  120. config.load(
  121. build_config_details({
  122. 'version': 18,
  123. 'services': {
  124. 'foo': {'image': 'busybox'}
  125. }
  126. }, 'working_dir', 'filename.yml')
  127. )
  128. with self.assertRaises(ConfigurationError):
  129. config.load(
  130. build_config_details({
  131. 'version': 'two point oh',
  132. 'services': {
  133. 'foo': {'image': 'busybox'}
  134. }
  135. }, 'working_dir', 'filename.yml')
  136. )
  137. def test_load_throws_error_when_not_dict(self):
  138. with self.assertRaises(ConfigurationError):
  139. config.load(
  140. build_config_details(
  141. {'web': 'busybox:latest'},
  142. 'working_dir',
  143. 'filename.yml'
  144. )
  145. )
  146. def test_load_throws_error_when_not_dict_v2(self):
  147. with self.assertRaises(ConfigurationError):
  148. config.load(
  149. build_config_details(
  150. {'version': 2, 'services': {'web': 'busybox:latest'}},
  151. 'working_dir',
  152. 'filename.yml'
  153. )
  154. )
  155. def test_load_config_invalid_service_names(self):
  156. for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']:
  157. with pytest.raises(ConfigurationError) as exc:
  158. config.load(build_config_details(
  159. {invalid_name: {'image': 'busybox'}},
  160. 'working_dir',
  161. 'filename.yml'))
  162. assert 'Invalid service name \'%s\'' % invalid_name in exc.exconly()
  163. def test_config_invalid_service_names_v2(self):
  164. for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']:
  165. with pytest.raises(ConfigurationError) as exc:
  166. config.load(
  167. build_config_details({
  168. 'version': 2,
  169. 'services': {invalid_name: {'image': 'busybox'}}
  170. }, 'working_dir', 'filename.yml')
  171. )
  172. assert 'Invalid service name \'%s\'' % invalid_name in exc.exconly()
  173. def test_load_with_invalid_field_name(self):
  174. config_details = build_config_details(
  175. {'web': {'image': 'busybox', 'name': 'bogus'}},
  176. 'working_dir',
  177. 'filename.yml')
  178. with pytest.raises(ConfigurationError) as exc:
  179. config.load(config_details)
  180. error_msg = "Unsupported config option for 'web' service: 'name'"
  181. assert error_msg in exc.exconly()
  182. assert "Validation failed in file 'filename.yml'" in exc.exconly()
  183. def test_load_invalid_service_definition(self):
  184. config_details = build_config_details(
  185. {'web': 'wrong'},
  186. 'working_dir',
  187. 'filename.yml')
  188. with pytest.raises(ConfigurationError) as exc:
  189. config.load(config_details)
  190. error_msg = "service 'web' doesn't have any configuration options"
  191. assert error_msg in exc.exconly()
  192. def test_config_integer_service_name_raise_validation_error(self):
  193. expected_error_msg = ("In file 'filename.yml' service name: 1 needs to "
  194. "be a string, eg '1'")
  195. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  196. config.load(
  197. build_config_details(
  198. {1: {'image': 'busybox'}},
  199. 'working_dir',
  200. 'filename.yml'
  201. )
  202. )
  203. def test_config_integer_service_name_raise_validation_error_v2(self):
  204. expected_error_msg = ("In file 'filename.yml' service name: 1 needs to "
  205. "be a string, eg '1'")
  206. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  207. config.load(
  208. build_config_details(
  209. {
  210. 'version': 2,
  211. 'services': {1: {'image': 'busybox'}}
  212. },
  213. 'working_dir',
  214. 'filename.yml'
  215. )
  216. )
  217. def test_load_with_multiple_files_v1(self):
  218. base_file = config.ConfigFile(
  219. 'base.yaml',
  220. {
  221. 'web': {
  222. 'image': 'example/web',
  223. 'links': ['db'],
  224. },
  225. 'db': {
  226. 'image': 'example/db',
  227. },
  228. })
  229. override_file = config.ConfigFile(
  230. 'override.yaml',
  231. {
  232. 'web': {
  233. 'build': '/',
  234. 'volumes': ['/home/user/project:/code'],
  235. },
  236. })
  237. details = config.ConfigDetails('.', [base_file, override_file])
  238. service_dicts = config.load(details).services
  239. expected = [
  240. {
  241. 'name': 'web',
  242. 'build': os.path.abspath('/'),
  243. 'volumes': [VolumeSpec.parse('/home/user/project:/code')],
  244. 'links': ['db'],
  245. },
  246. {
  247. 'name': 'db',
  248. 'image': 'example/db',
  249. },
  250. ]
  251. assert service_sort(service_dicts) == service_sort(expected)
  252. def test_load_with_multiple_files_and_empty_override(self):
  253. base_file = config.ConfigFile(
  254. 'base.yml',
  255. {'web': {'image': 'example/web'}})
  256. override_file = config.ConfigFile('override.yml', None)
  257. details = config.ConfigDetails('.', [base_file, override_file])
  258. with pytest.raises(ConfigurationError) as exc:
  259. config.load(details)
  260. error_msg = "Top level object in 'override.yml' needs to be an object"
  261. assert error_msg in exc.exconly()
  262. def test_load_with_multiple_files_and_empty_override_v2(self):
  263. base_file = config.ConfigFile(
  264. 'base.yml',
  265. {'version': 2, 'services': {'web': {'image': 'example/web'}}})
  266. override_file = config.ConfigFile('override.yml', None)
  267. details = config.ConfigDetails('.', [base_file, override_file])
  268. with pytest.raises(ConfigurationError) as exc:
  269. config.load(details)
  270. error_msg = "Top level object in 'override.yml' needs to be an object"
  271. assert error_msg in exc.exconly()
  272. def test_load_with_multiple_files_and_empty_base(self):
  273. base_file = config.ConfigFile('base.yml', None)
  274. override_file = config.ConfigFile(
  275. 'override.yml',
  276. {'web': {'image': 'example/web'}})
  277. details = config.ConfigDetails('.', [base_file, override_file])
  278. with pytest.raises(ConfigurationError) as exc:
  279. config.load(details)
  280. assert "Top level object in 'base.yml' needs to be an object" in exc.exconly()
  281. def test_load_with_multiple_files_and_empty_base_v2(self):
  282. base_file = config.ConfigFile('base.yml', None)
  283. override_file = config.ConfigFile(
  284. 'override.tml',
  285. {'version': 2, 'services': {'web': {'image': 'example/web'}}}
  286. )
  287. details = config.ConfigDetails('.', [base_file, override_file])
  288. with pytest.raises(ConfigurationError) as exc:
  289. config.load(details)
  290. assert "Top level object in 'base.yml' needs to be an object" in exc.exconly()
  291. def test_load_with_multiple_files_and_extends_in_override_file(self):
  292. base_file = config.ConfigFile(
  293. 'base.yaml',
  294. {
  295. 'web': {'image': 'example/web'},
  296. })
  297. override_file = config.ConfigFile(
  298. 'override.yaml',
  299. {
  300. 'web': {
  301. 'extends': {
  302. 'file': 'common.yml',
  303. 'service': 'base',
  304. },
  305. 'volumes': ['/home/user/project:/code'],
  306. },
  307. })
  308. details = config.ConfigDetails('.', [base_file, override_file])
  309. tmpdir = py.test.ensuretemp('config_test')
  310. self.addCleanup(tmpdir.remove)
  311. tmpdir.join('common.yml').write("""
  312. base:
  313. labels: ['label=one']
  314. """)
  315. with tmpdir.as_cwd():
  316. service_dicts = config.load(details).services
  317. expected = [
  318. {
  319. 'name': 'web',
  320. 'image': 'example/web',
  321. 'volumes': [VolumeSpec.parse('/home/user/project:/code')],
  322. 'labels': {'label': 'one'},
  323. },
  324. ]
  325. self.assertEqual(service_sort(service_dicts), service_sort(expected))
  326. def test_load_with_multiple_files_and_invalid_override(self):
  327. base_file = config.ConfigFile(
  328. 'base.yaml',
  329. {'web': {'image': 'example/web'}})
  330. override_file = config.ConfigFile(
  331. 'override.yaml',
  332. {'bogus': 'thing'})
  333. details = config.ConfigDetails('.', [base_file, override_file])
  334. with pytest.raises(ConfigurationError) as exc:
  335. config.load(details)
  336. assert "service 'bogus' doesn't have any configuration" in exc.exconly()
  337. assert "In file 'override.yaml'" in exc.exconly()
  338. def test_load_sorts_in_dependency_order(self):
  339. config_details = build_config_details({
  340. 'web': {
  341. 'image': 'busybox:latest',
  342. 'links': ['db'],
  343. },
  344. 'db': {
  345. 'image': 'busybox:latest',
  346. 'volumes_from': ['volume:ro']
  347. },
  348. 'volume': {
  349. 'image': 'busybox:latest',
  350. 'volumes': ['/tmp'],
  351. }
  352. })
  353. services = config.load(config_details).services
  354. assert services[0]['name'] == 'volume'
  355. assert services[1]['name'] == 'db'
  356. assert services[2]['name'] == 'web'
  357. def test_load_with_multiple_files_v2(self):
  358. base_file = config.ConfigFile(
  359. 'base.yaml',
  360. {
  361. 'version': 2,
  362. 'services': {
  363. 'web': {
  364. 'image': 'example/web',
  365. },
  366. 'db': {
  367. 'image': 'example/db',
  368. }
  369. },
  370. })
  371. override_file = config.ConfigFile(
  372. 'override.yaml',
  373. {
  374. 'version': 2,
  375. 'services': {
  376. 'web': {
  377. 'build': '/',
  378. 'volumes': ['/home/user/project:/code'],
  379. },
  380. }
  381. })
  382. details = config.ConfigDetails('.', [base_file, override_file])
  383. service_dicts = config.load(details).services
  384. expected = [
  385. {
  386. 'name': 'web',
  387. 'build': os.path.abspath('/'),
  388. 'image': 'example/web',
  389. 'volumes': [VolumeSpec.parse('/home/user/project:/code')],
  390. },
  391. {
  392. 'name': 'db',
  393. 'image': 'example/db',
  394. },
  395. ]
  396. assert service_sort(service_dicts) == service_sort(expected)
  397. def test_config_valid_service_names(self):
  398. for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']:
  399. services = config.load(
  400. build_config_details(
  401. {valid_name: {'image': 'busybox'}},
  402. 'tests/fixtures/extends',
  403. 'common.yml')).services
  404. assert services[0]['name'] == valid_name
  405. def test_config_hint(self):
  406. expected_error_msg = "(did you mean 'privileged'?)"
  407. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  408. config.load(
  409. build_config_details(
  410. {
  411. 'foo': {'image': 'busybox', 'privilige': 'something'},
  412. },
  413. 'tests/fixtures/extends',
  414. 'filename.yml'
  415. )
  416. )
  417. def test_invalid_config_build_and_image_specified(self):
  418. expected_error_msg = "Service 'foo' has both an image and build path specified."
  419. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  420. config.load(
  421. build_config_details(
  422. {
  423. 'foo': {'image': 'busybox', 'build': '.'},
  424. },
  425. 'tests/fixtures/extends',
  426. 'filename.yml'
  427. )
  428. )
  429. def test_invalid_config_type_should_be_an_array(self):
  430. expected_error_msg = "Service 'foo' configuration key 'links' contains an invalid type, it should be an array"
  431. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  432. config.load(
  433. build_config_details(
  434. {
  435. 'foo': {'image': 'busybox', 'links': 'an_link'},
  436. },
  437. 'tests/fixtures/extends',
  438. 'filename.yml'
  439. )
  440. )
  441. def test_invalid_config_not_a_dictionary(self):
  442. expected_error_msg = ("Top level object in 'filename.yml' needs to be "
  443. "an object.")
  444. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  445. config.load(
  446. build_config_details(
  447. ['foo', 'lol'],
  448. 'tests/fixtures/extends',
  449. 'filename.yml'
  450. )
  451. )
  452. def test_invalid_config_not_unique_items(self):
  453. expected_error_msg = "has non-unique elements"
  454. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  455. config.load(
  456. build_config_details(
  457. {
  458. 'web': {'build': '.', 'devices': ['/dev/foo:/dev/foo', '/dev/foo:/dev/foo']}
  459. },
  460. 'tests/fixtures/extends',
  461. 'filename.yml'
  462. )
  463. )
  464. def test_invalid_list_of_strings_format(self):
  465. expected_error_msg = "Service 'web' configuration key 'command' contains 1"
  466. expected_error_msg += ", which is an invalid type, it should be a string"
  467. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  468. config.load(
  469. build_config_details(
  470. {
  471. 'web': {'build': '.', 'command': [1]}
  472. },
  473. 'tests/fixtures/extends',
  474. 'filename.yml'
  475. )
  476. )
  477. def test_load_config_dockerfile_without_build_raises_error(self):
  478. with pytest.raises(ConfigurationError) as exc:
  479. config.load(build_config_details({
  480. 'web': {
  481. 'image': 'busybox',
  482. 'dockerfile': 'Dockerfile.alt'
  483. }
  484. }))
  485. assert "Service 'web' has both an image and alternate Dockerfile." in exc.exconly()
  486. def test_config_extra_hosts_string_raises_validation_error(self):
  487. expected_error_msg = "Service 'web' configuration key 'extra_hosts' contains an invalid type"
  488. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  489. config.load(
  490. build_config_details(
  491. {'web': {
  492. 'image': 'busybox',
  493. 'extra_hosts': 'somehost:162.242.195.82'
  494. }},
  495. 'working_dir',
  496. 'filename.yml'
  497. )
  498. )
  499. def test_config_extra_hosts_list_of_dicts_validation_error(self):
  500. expected_error_msg = (
  501. "key 'extra_hosts' contains {\"somehost\": \"162.242.195.82\"}, "
  502. "which is an invalid type, it should be a string")
  503. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  504. config.load(
  505. build_config_details(
  506. {'web': {
  507. 'image': 'busybox',
  508. 'extra_hosts': [
  509. {'somehost': '162.242.195.82'},
  510. {'otherhost': '50.31.209.229'}
  511. ]
  512. }},
  513. 'working_dir',
  514. 'filename.yml'
  515. )
  516. )
  517. def test_config_ulimits_invalid_keys_validation_error(self):
  518. expected = ("Service 'web' configuration key 'ulimits' 'nofile' contains "
  519. "unsupported option: 'not_soft_or_hard'")
  520. with pytest.raises(ConfigurationError) as exc:
  521. config.load(build_config_details(
  522. {
  523. 'web': {
  524. 'image': 'busybox',
  525. 'ulimits': {
  526. 'nofile': {
  527. "not_soft_or_hard": 100,
  528. "soft": 10000,
  529. "hard": 20000,
  530. }
  531. }
  532. }
  533. },
  534. 'working_dir',
  535. 'filename.yml'))
  536. assert expected in exc.exconly()
  537. def test_config_ulimits_required_keys_validation_error(self):
  538. with pytest.raises(ConfigurationError) as exc:
  539. config.load(build_config_details(
  540. {
  541. 'web': {
  542. 'image': 'busybox',
  543. 'ulimits': {'nofile': {"soft": 10000}}
  544. }
  545. },
  546. 'working_dir',
  547. 'filename.yml'))
  548. assert "Service 'web' configuration key 'ulimits' 'nofile'" in exc.exconly()
  549. assert "'hard' is a required property" in exc.exconly()
  550. def test_config_ulimits_soft_greater_than_hard_error(self):
  551. expected = "cannot contain a 'soft' value higher than 'hard' value"
  552. with pytest.raises(ConfigurationError) as exc:
  553. config.load(build_config_details(
  554. {
  555. 'web': {
  556. 'image': 'busybox',
  557. 'ulimits': {
  558. 'nofile': {"soft": 10000, "hard": 1000}
  559. }
  560. }
  561. },
  562. 'working_dir',
  563. 'filename.yml'))
  564. assert expected in exc.exconly()
  565. def test_valid_config_which_allows_two_type_definitions(self):
  566. expose_values = [["8000"], [8000]]
  567. for expose in expose_values:
  568. service = config.load(
  569. build_config_details(
  570. {'web': {
  571. 'image': 'busybox',
  572. 'expose': expose
  573. }},
  574. 'working_dir',
  575. 'filename.yml'
  576. )
  577. ).services
  578. self.assertEqual(service[0]['expose'], expose)
  579. def test_valid_config_oneof_string_or_list(self):
  580. entrypoint_values = [["sh"], "sh"]
  581. for entrypoint in entrypoint_values:
  582. service = config.load(
  583. build_config_details(
  584. {'web': {
  585. 'image': 'busybox',
  586. 'entrypoint': entrypoint
  587. }},
  588. 'working_dir',
  589. 'filename.yml'
  590. )
  591. ).services
  592. self.assertEqual(service[0]['entrypoint'], entrypoint)
  593. @mock.patch('compose.config.validation.log')
  594. def test_logs_warning_for_boolean_in_environment(self, mock_logging):
  595. expected_warning_msg = "There is a boolean value in the 'environment' key."
  596. config.load(
  597. build_config_details(
  598. {'web': {
  599. 'image': 'busybox',
  600. 'environment': {'SHOW_STUFF': True}
  601. }},
  602. 'working_dir',
  603. 'filename.yml'
  604. )
  605. )
  606. self.assertTrue(mock_logging.warn.called)
  607. self.assertTrue(expected_warning_msg in mock_logging.warn.call_args[0][0])
  608. def test_config_valid_environment_dict_key_contains_dashes(self):
  609. services = config.load(
  610. build_config_details(
  611. {'web': {
  612. 'image': 'busybox',
  613. 'environment': {'SPRING_JPA_HIBERNATE_DDL-AUTO': 'none'}
  614. }},
  615. 'working_dir',
  616. 'filename.yml'
  617. )
  618. ).services
  619. self.assertEqual(services[0]['environment']['SPRING_JPA_HIBERNATE_DDL-AUTO'], 'none')
  620. def test_load_yaml_with_yaml_error(self):
  621. tmpdir = py.test.ensuretemp('invalid_yaml_test')
  622. self.addCleanup(tmpdir.remove)
  623. invalid_yaml_file = tmpdir.join('docker-compose.yml')
  624. invalid_yaml_file.write("""
  625. web:
  626. this is bogus: ok: what
  627. """)
  628. with pytest.raises(ConfigurationError) as exc:
  629. config.load_yaml(str(invalid_yaml_file))
  630. assert 'line 3, column 32' in exc.exconly()
  631. def test_validate_extra_hosts_invalid(self):
  632. with pytest.raises(ConfigurationError) as exc:
  633. config.load(build_config_details({
  634. 'web': {
  635. 'image': 'alpine',
  636. 'extra_hosts': "www.example.com: 192.168.0.17",
  637. }
  638. }))
  639. assert "'extra_hosts' contains an invalid type" in exc.exconly()
  640. def test_validate_extra_hosts_invalid_list(self):
  641. with pytest.raises(ConfigurationError) as exc:
  642. config.load(build_config_details({
  643. 'web': {
  644. 'image': 'alpine',
  645. 'extra_hosts': [
  646. {'www.example.com': '192.168.0.17'},
  647. {'api.example.com': '192.168.0.18'}
  648. ],
  649. }
  650. }))
  651. assert "which is an invalid type" in exc.exconly()
  652. def test_normalize_dns_options(self):
  653. actual = config.load(build_config_details({
  654. 'web': {
  655. 'image': 'alpine',
  656. 'dns': '8.8.8.8',
  657. 'dns_search': 'domain.local',
  658. }
  659. }))
  660. assert actual.services == [
  661. {
  662. 'name': 'web',
  663. 'image': 'alpine',
  664. 'dns': ['8.8.8.8'],
  665. 'dns_search': ['domain.local'],
  666. }
  667. ]
  668. def test_merge_service_dicts_from_files_with_extends_in_base(self):
  669. base = {
  670. 'volumes': ['.:/app'],
  671. 'extends': {'service': 'app'}
  672. }
  673. override = {
  674. 'image': 'alpine:edge',
  675. }
  676. actual = config.merge_service_dicts_from_files(
  677. base,
  678. override,
  679. DEFAULT_VERSION)
  680. assert actual == {
  681. 'image': 'alpine:edge',
  682. 'volumes': ['.:/app'],
  683. 'extends': {'service': 'app'}
  684. }
  685. def test_merge_service_dicts_from_files_with_extends_in_override(self):
  686. base = {
  687. 'volumes': ['.:/app'],
  688. 'extends': {'service': 'app'}
  689. }
  690. override = {
  691. 'image': 'alpine:edge',
  692. 'extends': {'service': 'foo'}
  693. }
  694. actual = config.merge_service_dicts_from_files(
  695. base,
  696. override,
  697. DEFAULT_VERSION)
  698. assert actual == {
  699. 'image': 'alpine:edge',
  700. 'volumes': ['.:/app'],
  701. 'extends': {'service': 'foo'}
  702. }
  703. class PortsTest(unittest.TestCase):
  704. INVALID_PORTS_TYPES = [
  705. {"1": "8000"},
  706. False,
  707. "8000",
  708. 8000,
  709. ]
  710. NON_UNIQUE_SINGLE_PORTS = [
  711. ["8000", "8000"],
  712. ]
  713. INVALID_PORT_MAPPINGS = [
  714. ["8000-8001:8000"],
  715. ]
  716. VALID_SINGLE_PORTS = [
  717. ["8000"],
  718. ["8000/tcp"],
  719. ["8000", "9000"],
  720. [8000],
  721. [8000, 9000],
  722. ]
  723. VALID_PORT_MAPPINGS = [
  724. ["8000:8050"],
  725. ["49153-49154:3002-3003"],
  726. ]
  727. def test_config_invalid_ports_type_validation(self):
  728. for invalid_ports in self.INVALID_PORTS_TYPES:
  729. with pytest.raises(ConfigurationError) as exc:
  730. self.check_config({'ports': invalid_ports})
  731. assert "contains an invalid type" in exc.value.msg
  732. def test_config_non_unique_ports_validation(self):
  733. for invalid_ports in self.NON_UNIQUE_SINGLE_PORTS:
  734. with pytest.raises(ConfigurationError) as exc:
  735. self.check_config({'ports': invalid_ports})
  736. assert "non-unique" in exc.value.msg
  737. def test_config_invalid_ports_format_validation(self):
  738. for invalid_ports in self.INVALID_PORT_MAPPINGS:
  739. with pytest.raises(ConfigurationError) as exc:
  740. self.check_config({'ports': invalid_ports})
  741. assert "Port ranges don't match in length" in exc.value.msg
  742. def test_config_valid_ports_format_validation(self):
  743. for valid_ports in self.VALID_SINGLE_PORTS + self.VALID_PORT_MAPPINGS:
  744. self.check_config({'ports': valid_ports})
  745. def test_config_invalid_expose_type_validation(self):
  746. for invalid_expose in self.INVALID_PORTS_TYPES:
  747. with pytest.raises(ConfigurationError) as exc:
  748. self.check_config({'expose': invalid_expose})
  749. assert "contains an invalid type" in exc.value.msg
  750. def test_config_non_unique_expose_validation(self):
  751. for invalid_expose in self.NON_UNIQUE_SINGLE_PORTS:
  752. with pytest.raises(ConfigurationError) as exc:
  753. self.check_config({'expose': invalid_expose})
  754. assert "non-unique" in exc.value.msg
  755. def test_config_invalid_expose_format_validation(self):
  756. # Valid port mappings ARE NOT valid 'expose' entries
  757. for invalid_expose in self.INVALID_PORT_MAPPINGS + self.VALID_PORT_MAPPINGS:
  758. with pytest.raises(ConfigurationError) as exc:
  759. self.check_config({'expose': invalid_expose})
  760. assert "should be of the format" in exc.value.msg
  761. def test_config_valid_expose_format_validation(self):
  762. # Valid single ports ARE valid 'expose' entries
  763. for valid_expose in self.VALID_SINGLE_PORTS:
  764. self.check_config({'expose': valid_expose})
  765. def check_config(self, cfg):
  766. config.load(
  767. build_config_details(
  768. {'web': dict(image='busybox', **cfg)},
  769. 'working_dir',
  770. 'filename.yml'
  771. )
  772. )
  773. class InterpolationTest(unittest.TestCase):
  774. @mock.patch.dict(os.environ)
  775. def test_config_file_with_environment_variable(self):
  776. os.environ.update(
  777. IMAGE="busybox",
  778. HOST_PORT="80",
  779. LABEL_VALUE="myvalue",
  780. )
  781. service_dicts = config.load(
  782. config.find('tests/fixtures/environment-interpolation', None),
  783. ).services
  784. self.assertEqual(service_dicts, [
  785. {
  786. 'name': 'web',
  787. 'image': 'busybox',
  788. 'ports': ['80:8000'],
  789. 'labels': {'mylabel': 'myvalue'},
  790. 'hostname': 'host-',
  791. 'command': '${ESCAPED}',
  792. }
  793. ])
  794. @mock.patch.dict(os.environ)
  795. def test_unset_variable_produces_warning(self):
  796. os.environ.pop('FOO', None)
  797. os.environ.pop('BAR', None)
  798. config_details = build_config_details(
  799. {
  800. 'web': {
  801. 'image': '${FOO}',
  802. 'command': '${BAR}',
  803. 'container_name': '${BAR}',
  804. },
  805. },
  806. '.',
  807. None,
  808. )
  809. with mock.patch('compose.config.interpolation.log') as log:
  810. config.load(config_details)
  811. self.assertEqual(2, log.warn.call_count)
  812. warnings = sorted(args[0][0] for args in log.warn.call_args_list)
  813. self.assertIn('BAR', warnings[0])
  814. self.assertIn('FOO', warnings[1])
  815. @mock.patch.dict(os.environ)
  816. def test_invalid_interpolation(self):
  817. with self.assertRaises(config.ConfigurationError) as cm:
  818. config.load(
  819. build_config_details(
  820. {'web': {'image': '${'}},
  821. 'working_dir',
  822. 'filename.yml'
  823. )
  824. )
  825. self.assertIn('Invalid', cm.exception.msg)
  826. self.assertIn('for "image" option', cm.exception.msg)
  827. self.assertIn('in service "web"', cm.exception.msg)
  828. self.assertIn('"${"', cm.exception.msg)
  829. def test_empty_environment_key_allowed(self):
  830. service_dict = config.load(
  831. build_config_details(
  832. {
  833. 'web': {
  834. 'build': '.',
  835. 'environment': {
  836. 'POSTGRES_PASSWORD': ''
  837. },
  838. },
  839. },
  840. '.',
  841. None,
  842. )
  843. ).services[0]
  844. self.assertEquals(service_dict['environment']['POSTGRES_PASSWORD'], '')
  845. class VolumeConfigTest(unittest.TestCase):
  846. def test_no_binding(self):
  847. d = make_service_dict('foo', {'build': '.', 'volumes': ['/data']}, working_dir='.')
  848. self.assertEqual(d['volumes'], ['/data'])
  849. @mock.patch.dict(os.environ)
  850. def test_volume_binding_with_environment_variable(self):
  851. os.environ['VOLUME_PATH'] = '/host/path'
  852. d = config.load(
  853. build_config_details(
  854. {'foo': {'build': '.', 'volumes': ['${VOLUME_PATH}:/container/path']}},
  855. '.',
  856. None,
  857. )
  858. ).services[0]
  859. self.assertEqual(d['volumes'], [VolumeSpec.parse('/host/path:/container/path')])
  860. @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='posix paths')
  861. @mock.patch.dict(os.environ)
  862. def test_volume_binding_with_home(self):
  863. os.environ['HOME'] = '/home/user'
  864. d = make_service_dict('foo', {'build': '.', 'volumes': ['~:/container/path']}, working_dir='.')
  865. self.assertEqual(d['volumes'], ['/home/user:/container/path'])
  866. def test_name_does_not_expand(self):
  867. d = make_service_dict('foo', {'build': '.', 'volumes': ['mydatavolume:/data']}, working_dir='.')
  868. self.assertEqual(d['volumes'], ['mydatavolume:/data'])
  869. def test_absolute_posix_path_does_not_expand(self):
  870. d = make_service_dict('foo', {'build': '.', 'volumes': ['/var/lib/data:/data']}, working_dir='.')
  871. self.assertEqual(d['volumes'], ['/var/lib/data:/data'])
  872. def test_absolute_windows_path_does_not_expand(self):
  873. d = make_service_dict('foo', {'build': '.', 'volumes': ['c:\\data:/data']}, working_dir='.')
  874. self.assertEqual(d['volumes'], ['c:\\data:/data'])
  875. @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='posix paths')
  876. def test_relative_path_does_expand_posix(self):
  877. d = make_service_dict('foo', {'build': '.', 'volumes': ['./data:/data']}, working_dir='/home/me/myproject')
  878. self.assertEqual(d['volumes'], ['/home/me/myproject/data:/data'])
  879. d = make_service_dict('foo', {'build': '.', 'volumes': ['.:/data']}, working_dir='/home/me/myproject')
  880. self.assertEqual(d['volumes'], ['/home/me/myproject:/data'])
  881. d = make_service_dict('foo', {'build': '.', 'volumes': ['../otherproject:/data']}, working_dir='/home/me/myproject')
  882. self.assertEqual(d['volumes'], ['/home/me/otherproject:/data'])
  883. @pytest.mark.skipif(not IS_WINDOWS_PLATFORM, reason='windows paths')
  884. def test_relative_path_does_expand_windows(self):
  885. d = make_service_dict('foo', {'build': '.', 'volumes': ['./data:/data']}, working_dir='c:\\Users\\me\\myproject')
  886. self.assertEqual(d['volumes'], ['c:\\Users\\me\\myproject\\data:/data'])
  887. d = make_service_dict('foo', {'build': '.', 'volumes': ['.:/data']}, working_dir='c:\\Users\\me\\myproject')
  888. self.assertEqual(d['volumes'], ['c:\\Users\\me\\myproject:/data'])
  889. d = make_service_dict('foo', {'build': '.', 'volumes': ['../otherproject:/data']}, working_dir='c:\\Users\\me\\myproject')
  890. self.assertEqual(d['volumes'], ['c:\\Users\\me\\otherproject:/data'])
  891. @mock.patch.dict(os.environ)
  892. def test_home_directory_with_driver_does_not_expand(self):
  893. os.environ['NAME'] = 'surprise!'
  894. d = make_service_dict('foo', {
  895. 'build': '.',
  896. 'volumes': ['~:/data'],
  897. 'volume_driver': 'foodriver',
  898. }, working_dir='.')
  899. self.assertEqual(d['volumes'], ['~:/data'])
  900. def test_volume_path_with_non_ascii_directory(self):
  901. volume = u'/Füü/data:/data'
  902. container_path = config.resolve_volume_path(".", volume)
  903. self.assertEqual(container_path, volume)
  904. class MergePathMappingTest(object):
  905. def config_name(self):
  906. return ""
  907. def test_empty(self):
  908. service_dict = config.merge_service_dicts({}, {}, DEFAULT_VERSION)
  909. assert self.config_name() not in service_dict
  910. def test_no_override(self):
  911. service_dict = config.merge_service_dicts(
  912. {self.config_name(): ['/foo:/code', '/data']},
  913. {},
  914. DEFAULT_VERSION)
  915. assert set(service_dict[self.config_name()]) == set(['/foo:/code', '/data'])
  916. def test_no_base(self):
  917. service_dict = config.merge_service_dicts(
  918. {},
  919. {self.config_name(): ['/bar:/code']},
  920. DEFAULT_VERSION)
  921. assert set(service_dict[self.config_name()]) == set(['/bar:/code'])
  922. def test_override_explicit_path(self):
  923. service_dict = config.merge_service_dicts(
  924. {self.config_name(): ['/foo:/code', '/data']},
  925. {self.config_name(): ['/bar:/code']},
  926. DEFAULT_VERSION)
  927. assert set(service_dict[self.config_name()]) == set(['/bar:/code', '/data'])
  928. def test_add_explicit_path(self):
  929. service_dict = config.merge_service_dicts(
  930. {self.config_name(): ['/foo:/code', '/data']},
  931. {self.config_name(): ['/bar:/code', '/quux:/data']},
  932. DEFAULT_VERSION)
  933. assert set(service_dict[self.config_name()]) == set(['/bar:/code', '/quux:/data'])
  934. def test_remove_explicit_path(self):
  935. service_dict = config.merge_service_dicts(
  936. {self.config_name(): ['/foo:/code', '/quux:/data']},
  937. {self.config_name(): ['/bar:/code', '/data']},
  938. DEFAULT_VERSION)
  939. assert set(service_dict[self.config_name()]) == set(['/bar:/code', '/data'])
  940. class MergeVolumesTest(unittest.TestCase, MergePathMappingTest):
  941. def config_name(self):
  942. return 'volumes'
  943. class MergeDevicesTest(unittest.TestCase, MergePathMappingTest):
  944. def config_name(self):
  945. return 'devices'
  946. class BuildOrImageMergeTest(unittest.TestCase):
  947. def test_merge_build_or_image_no_override(self):
  948. self.assertEqual(
  949. config.merge_service_dicts({'build': '.'}, {}, V1),
  950. {'build': '.'},
  951. )
  952. self.assertEqual(
  953. config.merge_service_dicts({'image': 'redis'}, {}, V1),
  954. {'image': 'redis'},
  955. )
  956. def test_merge_build_or_image_override_with_same(self):
  957. self.assertEqual(
  958. config.merge_service_dicts({'build': '.'}, {'build': './web'}, V1),
  959. {'build': './web'},
  960. )
  961. self.assertEqual(
  962. config.merge_service_dicts({'image': 'redis'}, {'image': 'postgres'}, V1),
  963. {'image': 'postgres'},
  964. )
  965. def test_merge_build_or_image_override_with_other(self):
  966. self.assertEqual(
  967. config.merge_service_dicts({'build': '.'}, {'image': 'redis'}, V1),
  968. {'image': 'redis'},
  969. )
  970. self.assertEqual(
  971. config.merge_service_dicts({'image': 'redis'}, {'build': '.'}, V1),
  972. {'build': '.'},
  973. )
  974. class MergeListsTest(unittest.TestCase):
  975. def test_empty(self):
  976. assert 'ports' not in config.merge_service_dicts({}, {}, DEFAULT_VERSION)
  977. def test_no_override(self):
  978. service_dict = config.merge_service_dicts(
  979. {'ports': ['10:8000', '9000']},
  980. {},
  981. DEFAULT_VERSION)
  982. assert set(service_dict['ports']) == set(['10:8000', '9000'])
  983. def test_no_base(self):
  984. service_dict = config.merge_service_dicts(
  985. {},
  986. {'ports': ['10:8000', '9000']},
  987. DEFAULT_VERSION)
  988. assert set(service_dict['ports']) == set(['10:8000', '9000'])
  989. def test_add_item(self):
  990. service_dict = config.merge_service_dicts(
  991. {'ports': ['10:8000', '9000']},
  992. {'ports': ['20:8000']},
  993. DEFAULT_VERSION)
  994. assert set(service_dict['ports']) == set(['10:8000', '9000', '20:8000'])
  995. class MergeStringsOrListsTest(unittest.TestCase):
  996. def test_no_override(self):
  997. service_dict = config.merge_service_dicts(
  998. {'dns': '8.8.8.8'},
  999. {},
  1000. DEFAULT_VERSION)
  1001. assert set(service_dict['dns']) == set(['8.8.8.8'])
  1002. def test_no_base(self):
  1003. service_dict = config.merge_service_dicts(
  1004. {},
  1005. {'dns': '8.8.8.8'},
  1006. DEFAULT_VERSION)
  1007. assert set(service_dict['dns']) == set(['8.8.8.8'])
  1008. def test_add_string(self):
  1009. service_dict = config.merge_service_dicts(
  1010. {'dns': ['8.8.8.8']},
  1011. {'dns': '9.9.9.9'},
  1012. DEFAULT_VERSION)
  1013. assert set(service_dict['dns']) == set(['8.8.8.8', '9.9.9.9'])
  1014. def test_add_list(self):
  1015. service_dict = config.merge_service_dicts(
  1016. {'dns': '8.8.8.8'},
  1017. {'dns': ['9.9.9.9']},
  1018. DEFAULT_VERSION)
  1019. assert set(service_dict['dns']) == set(['8.8.8.8', '9.9.9.9'])
  1020. class MergeLabelsTest(unittest.TestCase):
  1021. def test_empty(self):
  1022. assert 'labels' not in config.merge_service_dicts({}, {}, DEFAULT_VERSION)
  1023. def test_no_override(self):
  1024. service_dict = config.merge_service_dicts(
  1025. make_service_dict('foo', {'build': '.', 'labels': ['foo=1', 'bar']}, 'tests/'),
  1026. make_service_dict('foo', {'build': '.'}, 'tests/'),
  1027. DEFAULT_VERSION)
  1028. assert service_dict['labels'] == {'foo': '1', 'bar': ''}
  1029. def test_no_base(self):
  1030. service_dict = config.merge_service_dicts(
  1031. make_service_dict('foo', {'build': '.'}, 'tests/'),
  1032. make_service_dict('foo', {'build': '.', 'labels': ['foo=2']}, 'tests/'),
  1033. DEFAULT_VERSION)
  1034. assert service_dict['labels'] == {'foo': '2'}
  1035. def test_override_explicit_value(self):
  1036. service_dict = config.merge_service_dicts(
  1037. make_service_dict('foo', {'build': '.', 'labels': ['foo=1', 'bar']}, 'tests/'),
  1038. make_service_dict('foo', {'build': '.', 'labels': ['foo=2']}, 'tests/'),
  1039. DEFAULT_VERSION)
  1040. assert service_dict['labels'] == {'foo': '2', 'bar': ''}
  1041. def test_add_explicit_value(self):
  1042. service_dict = config.merge_service_dicts(
  1043. make_service_dict('foo', {'build': '.', 'labels': ['foo=1', 'bar']}, 'tests/'),
  1044. make_service_dict('foo', {'build': '.', 'labels': ['bar=2']}, 'tests/'),
  1045. DEFAULT_VERSION)
  1046. assert service_dict['labels'] == {'foo': '1', 'bar': '2'}
  1047. def test_remove_explicit_value(self):
  1048. service_dict = config.merge_service_dicts(
  1049. make_service_dict('foo', {'build': '.', 'labels': ['foo=1', 'bar=2']}, 'tests/'),
  1050. make_service_dict('foo', {'build': '.', 'labels': ['bar']}, 'tests/'),
  1051. DEFAULT_VERSION)
  1052. assert service_dict['labels'] == {'foo': '1', 'bar': ''}
  1053. class MemoryOptionsTest(unittest.TestCase):
  1054. def test_validation_fails_with_just_memswap_limit(self):
  1055. """
  1056. When you set a 'memswap_limit' it is invalid config unless you also set
  1057. a mem_limit
  1058. """
  1059. expected_error_msg = (
  1060. "Service 'foo' configuration key 'memswap_limit' is invalid: when "
  1061. "defining 'memswap_limit' you must set 'mem_limit' as well"
  1062. )
  1063. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  1064. config.load(
  1065. build_config_details(
  1066. {
  1067. 'foo': {'image': 'busybox', 'memswap_limit': 2000000},
  1068. },
  1069. 'tests/fixtures/extends',
  1070. 'filename.yml'
  1071. )
  1072. )
  1073. def test_validation_with_correct_memswap_values(self):
  1074. service_dict = config.load(
  1075. build_config_details(
  1076. {'foo': {'image': 'busybox', 'mem_limit': 1000000, 'memswap_limit': 2000000}},
  1077. 'tests/fixtures/extends',
  1078. 'common.yml'
  1079. )
  1080. ).services
  1081. self.assertEqual(service_dict[0]['memswap_limit'], 2000000)
  1082. def test_memswap_can_be_a_string(self):
  1083. service_dict = config.load(
  1084. build_config_details(
  1085. {'foo': {'image': 'busybox', 'mem_limit': "1G", 'memswap_limit': "512M"}},
  1086. 'tests/fixtures/extends',
  1087. 'common.yml'
  1088. )
  1089. ).services
  1090. self.assertEqual(service_dict[0]['memswap_limit'], "512M")
  1091. class EnvTest(unittest.TestCase):
  1092. def test_parse_environment_as_list(self):
  1093. environment = [
  1094. 'NORMAL=F1',
  1095. 'CONTAINS_EQUALS=F=2',
  1096. 'TRAILING_EQUALS=',
  1097. ]
  1098. self.assertEqual(
  1099. config.parse_environment(environment),
  1100. {'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''},
  1101. )
  1102. def test_parse_environment_as_dict(self):
  1103. environment = {
  1104. 'NORMAL': 'F1',
  1105. 'CONTAINS_EQUALS': 'F=2',
  1106. 'TRAILING_EQUALS': None,
  1107. }
  1108. self.assertEqual(config.parse_environment(environment), environment)
  1109. def test_parse_environment_invalid(self):
  1110. with self.assertRaises(ConfigurationError):
  1111. config.parse_environment('a=b')
  1112. def test_parse_environment_empty(self):
  1113. self.assertEqual(config.parse_environment(None), {})
  1114. @mock.patch.dict(os.environ)
  1115. def test_resolve_environment(self):
  1116. os.environ['FILE_DEF'] = 'E1'
  1117. os.environ['FILE_DEF_EMPTY'] = 'E2'
  1118. os.environ['ENV_DEF'] = 'E3'
  1119. service_dict = {
  1120. 'build': '.',
  1121. 'environment': {
  1122. 'FILE_DEF': 'F1',
  1123. 'FILE_DEF_EMPTY': '',
  1124. 'ENV_DEF': None,
  1125. 'NO_DEF': None
  1126. },
  1127. }
  1128. self.assertEqual(
  1129. resolve_environment(service_dict),
  1130. {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''},
  1131. )
  1132. def test_resolve_environment_from_env_file(self):
  1133. self.assertEqual(
  1134. resolve_environment({'env_file': ['tests/fixtures/env/one.env']}),
  1135. {'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'bar'},
  1136. )
  1137. def test_resolve_environment_with_multiple_env_files(self):
  1138. service_dict = {
  1139. 'env_file': [
  1140. 'tests/fixtures/env/one.env',
  1141. 'tests/fixtures/env/two.env'
  1142. ]
  1143. }
  1144. self.assertEqual(
  1145. resolve_environment(service_dict),
  1146. {'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'},
  1147. )
  1148. def test_resolve_environment_nonexistent_file(self):
  1149. with pytest.raises(ConfigurationError) as exc:
  1150. config.load(build_config_details(
  1151. {'foo': {'image': 'example', 'env_file': 'nonexistent.env'}},
  1152. working_dir='tests/fixtures/env'))
  1153. assert 'Couldn\'t find env file' in exc.exconly()
  1154. assert 'nonexistent.env' in exc.exconly()
  1155. @mock.patch.dict(os.environ)
  1156. def test_resolve_environment_from_env_file_with_empty_values(self):
  1157. os.environ['FILE_DEF'] = 'E1'
  1158. os.environ['FILE_DEF_EMPTY'] = 'E2'
  1159. os.environ['ENV_DEF'] = 'E3'
  1160. self.assertEqual(
  1161. resolve_environment({'env_file': ['tests/fixtures/env/resolve.env']}),
  1162. {
  1163. 'FILE_DEF': u'bär',
  1164. 'FILE_DEF_EMPTY': '',
  1165. 'ENV_DEF': 'E3',
  1166. 'NO_DEF': ''
  1167. },
  1168. )
  1169. @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
  1170. @mock.patch.dict(os.environ)
  1171. def test_resolve_path(self):
  1172. os.environ['HOSTENV'] = '/tmp'
  1173. os.environ['CONTAINERENV'] = '/host/tmp'
  1174. service_dict = config.load(
  1175. build_config_details(
  1176. {'foo': {'build': '.', 'volumes': ['$HOSTENV:$CONTAINERENV']}},
  1177. "tests/fixtures/env",
  1178. )
  1179. ).services[0]
  1180. self.assertEqual(
  1181. set(service_dict['volumes']),
  1182. set([VolumeSpec.parse('/tmp:/host/tmp')]))
  1183. service_dict = config.load(
  1184. build_config_details(
  1185. {'foo': {'build': '.', 'volumes': ['/opt${HOSTENV}:/opt${CONTAINERENV}']}},
  1186. "tests/fixtures/env",
  1187. )
  1188. ).services[0]
  1189. self.assertEqual(
  1190. set(service_dict['volumes']),
  1191. set([VolumeSpec.parse('/opt/tmp:/opt/host/tmp')]))
  1192. def load_from_filename(filename):
  1193. return config.load(config.find('.', [filename])).services
  1194. class ExtendsTest(unittest.TestCase):
  1195. def test_extends(self):
  1196. service_dicts = load_from_filename('tests/fixtures/extends/docker-compose.yml')
  1197. self.assertEqual(service_sort(service_dicts), service_sort([
  1198. {
  1199. 'name': 'mydb',
  1200. 'image': 'busybox',
  1201. 'command': 'top',
  1202. },
  1203. {
  1204. 'name': 'myweb',
  1205. 'image': 'busybox',
  1206. 'command': 'top',
  1207. 'links': ['mydb:db'],
  1208. 'environment': {
  1209. "FOO": "1",
  1210. "BAR": "2",
  1211. "BAZ": "2",
  1212. },
  1213. }
  1214. ]))
  1215. def test_merging_env_labels_ulimits(self):
  1216. service_dicts = load_from_filename('tests/fixtures/extends/common-env-labels-ulimits.yml')
  1217. self.assertEqual(service_sort(service_dicts), service_sort([
  1218. {
  1219. 'name': 'web',
  1220. 'image': 'busybox',
  1221. 'command': '/bin/true',
  1222. 'environment': {
  1223. "FOO": "2",
  1224. "BAR": "1",
  1225. "BAZ": "3",
  1226. },
  1227. 'labels': {'label': 'one'},
  1228. 'ulimits': {'nproc': 65535, 'memlock': {'soft': 1024, 'hard': 2048}}
  1229. }
  1230. ]))
  1231. def test_nested(self):
  1232. service_dicts = load_from_filename('tests/fixtures/extends/nested.yml')
  1233. self.assertEqual(service_dicts, [
  1234. {
  1235. 'name': 'myweb',
  1236. 'image': 'busybox',
  1237. 'command': '/bin/true',
  1238. 'environment': {
  1239. "FOO": "2",
  1240. "BAR": "2",
  1241. },
  1242. },
  1243. ])
  1244. def test_self_referencing_file(self):
  1245. """
  1246. We specify a 'file' key that is the filename we're already in.
  1247. """
  1248. service_dicts = load_from_filename('tests/fixtures/extends/specify-file-as-self.yml')
  1249. self.assertEqual(service_sort(service_dicts), service_sort([
  1250. {
  1251. 'environment':
  1252. {
  1253. 'YEP': '1', 'BAR': '1', 'BAZ': '3'
  1254. },
  1255. 'image': 'busybox',
  1256. 'name': 'myweb'
  1257. },
  1258. {
  1259. 'environment':
  1260. {'YEP': '1'},
  1261. 'image': 'busybox',
  1262. 'name': 'otherweb'
  1263. },
  1264. {
  1265. 'environment':
  1266. {'YEP': '1', 'BAZ': '3'},
  1267. 'image': 'busybox',
  1268. 'name': 'web'
  1269. }
  1270. ]))
  1271. def test_circular(self):
  1272. with pytest.raises(config.CircularReference) as exc:
  1273. load_from_filename('tests/fixtures/extends/circle-1.yml')
  1274. path = [
  1275. (os.path.basename(filename), service_name)
  1276. for (filename, service_name) in exc.value.trail
  1277. ]
  1278. expected = [
  1279. ('circle-1.yml', 'web'),
  1280. ('circle-2.yml', 'other'),
  1281. ('circle-1.yml', 'web'),
  1282. ]
  1283. self.assertEqual(path, expected)
  1284. def test_extends_validation_empty_dictionary(self):
  1285. with self.assertRaisesRegexp(ConfigurationError, 'service'):
  1286. config.load(
  1287. build_config_details(
  1288. {
  1289. 'web': {'image': 'busybox', 'extends': {}},
  1290. },
  1291. 'tests/fixtures/extends',
  1292. 'filename.yml'
  1293. )
  1294. )
  1295. def test_extends_validation_missing_service_key(self):
  1296. with self.assertRaisesRegexp(ConfigurationError, "'service' is a required property"):
  1297. config.load(
  1298. build_config_details(
  1299. {
  1300. 'web': {'image': 'busybox', 'extends': {'file': 'common.yml'}},
  1301. },
  1302. 'tests/fixtures/extends',
  1303. 'filename.yml'
  1304. )
  1305. )
  1306. def test_extends_validation_invalid_key(self):
  1307. expected_error_msg = (
  1308. "Service 'web' configuration key 'extends' "
  1309. "contains unsupported option: 'rogue_key'"
  1310. )
  1311. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  1312. config.load(
  1313. build_config_details(
  1314. {
  1315. 'web': {
  1316. 'image': 'busybox',
  1317. 'extends': {
  1318. 'file': 'common.yml',
  1319. 'service': 'web',
  1320. 'rogue_key': 'is not allowed'
  1321. }
  1322. },
  1323. },
  1324. 'tests/fixtures/extends',
  1325. 'filename.yml'
  1326. )
  1327. )
  1328. def test_extends_validation_sub_property_key(self):
  1329. expected_error_msg = (
  1330. "Service 'web' configuration key 'extends' 'file' contains 1, "
  1331. "which is an invalid type, it should be a string"
  1332. )
  1333. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  1334. config.load(
  1335. build_config_details(
  1336. {
  1337. 'web': {
  1338. 'image': 'busybox',
  1339. 'extends': {
  1340. 'file': 1,
  1341. 'service': 'web',
  1342. }
  1343. },
  1344. },
  1345. 'tests/fixtures/extends',
  1346. 'filename.yml'
  1347. )
  1348. )
  1349. def test_extends_validation_no_file_key_no_filename_set(self):
  1350. dictionary = {'extends': {'service': 'web'}}
  1351. def load_config():
  1352. return make_service_dict('myweb', dictionary, working_dir='tests/fixtures/extends')
  1353. self.assertRaisesRegexp(ConfigurationError, 'file', load_config)
  1354. def test_extends_validation_valid_config(self):
  1355. service = config.load(
  1356. build_config_details(
  1357. {
  1358. 'web': {'image': 'busybox', 'extends': {'service': 'web', 'file': 'common.yml'}},
  1359. },
  1360. 'tests/fixtures/extends',
  1361. 'common.yml'
  1362. )
  1363. ).services
  1364. self.assertEquals(len(service), 1)
  1365. self.assertIsInstance(service[0], dict)
  1366. self.assertEquals(service[0]['command'], "/bin/true")
  1367. def test_extended_service_with_invalid_config(self):
  1368. with pytest.raises(ConfigurationError) as exc:
  1369. load_from_filename('tests/fixtures/extends/service-with-invalid-schema.yml')
  1370. assert (
  1371. "Service 'myweb' has neither an image nor a build path specified" in
  1372. exc.exconly()
  1373. )
  1374. def test_extended_service_with_valid_config(self):
  1375. service = load_from_filename('tests/fixtures/extends/service-with-valid-composite-extends.yml')
  1376. self.assertEquals(service[0]['command'], "top")
  1377. def test_extends_file_defaults_to_self(self):
  1378. """
  1379. Test not specifying a file in our extends options that the
  1380. config is valid and correctly extends from itself.
  1381. """
  1382. service_dicts = load_from_filename('tests/fixtures/extends/no-file-specified.yml')
  1383. self.assertEqual(service_sort(service_dicts), service_sort([
  1384. {
  1385. 'name': 'myweb',
  1386. 'image': 'busybox',
  1387. 'environment': {
  1388. "BAR": "1",
  1389. "BAZ": "3",
  1390. }
  1391. },
  1392. {
  1393. 'name': 'web',
  1394. 'image': 'busybox',
  1395. 'environment': {
  1396. "BAZ": "3",
  1397. }
  1398. }
  1399. ]))
  1400. def test_invalid_links_in_extended_service(self):
  1401. expected_error_msg = "services with 'links' cannot be extended"
  1402. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  1403. load_from_filename('tests/fixtures/extends/invalid-links.yml')
  1404. def test_invalid_volumes_from_in_extended_service(self):
  1405. expected_error_msg = "services with 'volumes_from' cannot be extended"
  1406. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  1407. load_from_filename('tests/fixtures/extends/invalid-volumes.yml')
  1408. def test_invalid_net_in_extended_service(self):
  1409. expected_error_msg = "services with 'net: container' cannot be extended"
  1410. with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
  1411. load_from_filename('tests/fixtures/extends/invalid-net.yml')
  1412. @mock.patch.dict(os.environ)
  1413. def test_valid_interpolation_in_extended_service(self):
  1414. os.environ.update(
  1415. HOSTNAME_VALUE="penguin",
  1416. )
  1417. expected_interpolated_value = "host-penguin"
  1418. service_dicts = load_from_filename('tests/fixtures/extends/valid-interpolation.yml')
  1419. for service in service_dicts:
  1420. self.assertTrue(service['hostname'], expected_interpolated_value)
  1421. @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
  1422. def test_volume_path(self):
  1423. dicts = load_from_filename('tests/fixtures/volume-path/docker-compose.yml')
  1424. paths = [
  1425. VolumeSpec(
  1426. os.path.abspath('tests/fixtures/volume-path/common/foo'),
  1427. '/foo',
  1428. 'rw'),
  1429. VolumeSpec(
  1430. os.path.abspath('tests/fixtures/volume-path/bar'),
  1431. '/bar',
  1432. 'rw')
  1433. ]
  1434. self.assertEqual(set(dicts[0]['volumes']), set(paths))
  1435. def test_parent_build_path_dne(self):
  1436. child = load_from_filename('tests/fixtures/extends/nonexistent-path-child.yml')
  1437. self.assertEqual(child, [
  1438. {
  1439. 'name': 'dnechild',
  1440. 'image': 'busybox',
  1441. 'command': '/bin/true',
  1442. 'environment': {
  1443. "FOO": "1",
  1444. "BAR": "2",
  1445. },
  1446. },
  1447. ])
  1448. def test_load_throws_error_when_base_service_does_not_exist(self):
  1449. err_msg = r'''Cannot extend service 'foo' in .*: Service not found'''
  1450. with self.assertRaisesRegexp(ConfigurationError, err_msg):
  1451. load_from_filename('tests/fixtures/extends/nonexistent-service.yml')
  1452. def test_partial_service_config_in_extends_is_still_valid(self):
  1453. dicts = load_from_filename('tests/fixtures/extends/valid-common-config.yml')
  1454. self.assertEqual(dicts[0]['environment'], {'FOO': '1'})
  1455. def test_extended_service_with_verbose_and_shorthand_way(self):
  1456. services = load_from_filename('tests/fixtures/extends/verbose-and-shorthand.yml')
  1457. self.assertEqual(service_sort(services), service_sort([
  1458. {
  1459. 'name': 'base',
  1460. 'image': 'busybox',
  1461. 'environment': {'BAR': '1'},
  1462. },
  1463. {
  1464. 'name': 'verbose',
  1465. 'image': 'busybox',
  1466. 'environment': {'BAR': '1', 'FOO': '1'},
  1467. },
  1468. {
  1469. 'name': 'shorthand',
  1470. 'image': 'busybox',
  1471. 'environment': {'BAR': '1', 'FOO': '2'},
  1472. },
  1473. ]))
  1474. def test_extends_with_environment_and_env_files(self):
  1475. tmpdir = py.test.ensuretemp('test_extends_with_environment')
  1476. self.addCleanup(tmpdir.remove)
  1477. commondir = tmpdir.mkdir('common')
  1478. commondir.join('base.yml').write("""
  1479. app:
  1480. image: 'example/app'
  1481. env_file:
  1482. - 'envs'
  1483. environment:
  1484. - SECRET
  1485. - TEST_ONE=common
  1486. - TEST_TWO=common
  1487. """)
  1488. tmpdir.join('docker-compose.yml').write("""
  1489. ext:
  1490. extends:
  1491. file: common/base.yml
  1492. service: app
  1493. env_file:
  1494. - 'envs'
  1495. environment:
  1496. - THING
  1497. - TEST_ONE=top
  1498. """)
  1499. commondir.join('envs').write("""
  1500. COMMON_ENV_FILE
  1501. TEST_ONE=common-env-file
  1502. TEST_TWO=common-env-file
  1503. TEST_THREE=common-env-file
  1504. TEST_FOUR=common-env-file
  1505. """)
  1506. tmpdir.join('envs').write("""
  1507. TOP_ENV_FILE
  1508. TEST_ONE=top-env-file
  1509. TEST_TWO=top-env-file
  1510. TEST_THREE=top-env-file
  1511. """)
  1512. expected = [
  1513. {
  1514. 'name': 'ext',
  1515. 'image': 'example/app',
  1516. 'environment': {
  1517. 'SECRET': 'secret',
  1518. 'TOP_ENV_FILE': 'secret',
  1519. 'COMMON_ENV_FILE': 'secret',
  1520. 'THING': 'thing',
  1521. 'TEST_ONE': 'top',
  1522. 'TEST_TWO': 'common',
  1523. 'TEST_THREE': 'top-env-file',
  1524. 'TEST_FOUR': 'common-env-file',
  1525. },
  1526. },
  1527. ]
  1528. with mock.patch.dict(os.environ):
  1529. os.environ['SECRET'] = 'secret'
  1530. os.environ['THING'] = 'thing'
  1531. os.environ['COMMON_ENV_FILE'] = 'secret'
  1532. os.environ['TOP_ENV_FILE'] = 'secret'
  1533. config = load_from_filename(str(tmpdir.join('docker-compose.yml')))
  1534. assert config == expected
  1535. @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
  1536. class ExpandPathTest(unittest.TestCase):
  1537. working_dir = '/home/user/somedir'
  1538. def test_expand_path_normal(self):
  1539. result = config.expand_path(self.working_dir, 'myfile')
  1540. self.assertEqual(result, self.working_dir + '/' + 'myfile')
  1541. def test_expand_path_absolute(self):
  1542. abs_path = '/home/user/otherdir/somefile'
  1543. result = config.expand_path(self.working_dir, abs_path)
  1544. self.assertEqual(result, abs_path)
  1545. def test_expand_path_with_tilde(self):
  1546. test_path = '~/otherdir/somefile'
  1547. with mock.patch.dict(os.environ):
  1548. os.environ['HOME'] = user_path = '/home/user/'
  1549. result = config.expand_path(self.working_dir, test_path)
  1550. self.assertEqual(result, user_path + 'otherdir/somefile')
  1551. class VolumePathTest(unittest.TestCase):
  1552. @pytest.mark.xfail((not IS_WINDOWS_PLATFORM), reason='does not have a drive')
  1553. def test_split_path_mapping_with_windows_path(self):
  1554. windows_volume_path = "c:\\Users\\msamblanet\\Documents\\anvil\\connect\\config:/opt/connect/config:ro"
  1555. expected_mapping = (
  1556. "/opt/connect/config:ro",
  1557. "c:\\Users\\msamblanet\\Documents\\anvil\\connect\\config"
  1558. )
  1559. mapping = config.split_path_mapping(windows_volume_path)
  1560. self.assertEqual(mapping, expected_mapping)
  1561. @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
  1562. class BuildPathTest(unittest.TestCase):
  1563. def setUp(self):
  1564. self.abs_context_path = os.path.join(os.getcwd(), 'tests/fixtures/build-ctx')
  1565. def test_nonexistent_path(self):
  1566. with self.assertRaises(ConfigurationError):
  1567. config.load(
  1568. build_config_details(
  1569. {
  1570. 'foo': {'build': 'nonexistent.path'},
  1571. },
  1572. 'working_dir',
  1573. 'filename.yml'
  1574. )
  1575. )
  1576. def test_relative_path(self):
  1577. relative_build_path = '../build-ctx/'
  1578. service_dict = make_service_dict(
  1579. 'relpath',
  1580. {'build': relative_build_path},
  1581. working_dir='tests/fixtures/build-path'
  1582. )
  1583. self.assertEquals(service_dict['build'], self.abs_context_path)
  1584. def test_absolute_path(self):
  1585. service_dict = make_service_dict(
  1586. 'abspath',
  1587. {'build': self.abs_context_path},
  1588. working_dir='tests/fixtures/build-path'
  1589. )
  1590. self.assertEquals(service_dict['build'], self.abs_context_path)
  1591. def test_from_file(self):
  1592. service_dict = load_from_filename('tests/fixtures/build-path/docker-compose.yml')
  1593. self.assertEquals(service_dict, [{'name': 'foo', 'build': self.abs_context_path}])
  1594. def test_valid_url_in_build_path(self):
  1595. valid_urls = [
  1596. 'git://github.com/docker/docker',
  1597. '[email protected]:docker/docker.git',
  1598. '[email protected]:atlassianlabs/atlassian-docker.git',
  1599. 'https://github.com/docker/docker.git',
  1600. 'http://github.com/docker/docker.git',
  1601. 'github.com/docker/docker.git',
  1602. ]
  1603. for valid_url in valid_urls:
  1604. service_dict = config.load(build_config_details({
  1605. 'validurl': {'build': valid_url},
  1606. }, '.', None)).services
  1607. assert service_dict[0]['build'] == valid_url
  1608. def test_invalid_url_in_build_path(self):
  1609. invalid_urls = [
  1610. 'example.com/bogus',
  1611. 'ftp://example.com/',
  1612. '/path/does/not/exist',
  1613. ]
  1614. for invalid_url in invalid_urls:
  1615. with pytest.raises(ConfigurationError) as exc:
  1616. config.load(build_config_details({
  1617. 'invalidurl': {'build': invalid_url},
  1618. }, '.', None))
  1619. assert 'build path' in exc.exconly()
  1620. class GetDefaultConfigFilesTestCase(unittest.TestCase):
  1621. files = [
  1622. 'docker-compose.yml',
  1623. 'docker-compose.yaml',
  1624. ]
  1625. def test_get_config_path_default_file_in_basedir(self):
  1626. for index, filename in enumerate(self.files):
  1627. self.assertEqual(
  1628. filename,
  1629. get_config_filename_for_files(self.files[index:]))
  1630. with self.assertRaises(config.ComposeFileNotFound):
  1631. get_config_filename_for_files([])
  1632. def test_get_config_path_default_file_in_parent_dir(self):
  1633. """Test with files placed in the subdir"""
  1634. def get_config_in_subdir(files):
  1635. return get_config_filename_for_files(files, subdir=True)
  1636. for index, filename in enumerate(self.files):
  1637. self.assertEqual(filename, get_config_in_subdir(self.files[index:]))
  1638. with self.assertRaises(config.ComposeFileNotFound):
  1639. get_config_in_subdir([])
  1640. def get_config_filename_for_files(filenames, subdir=None):
  1641. def make_files(dirname, filenames):
  1642. for fname in filenames:
  1643. with open(os.path.join(dirname, fname), 'w') as f:
  1644. f.write('')
  1645. project_dir = tempfile.mkdtemp()
  1646. try:
  1647. make_files(project_dir, filenames)
  1648. if subdir:
  1649. base_dir = tempfile.mkdtemp(dir=project_dir)
  1650. else:
  1651. base_dir = project_dir
  1652. filename, = config.get_default_config_files(base_dir)
  1653. return os.path.basename(filename)
  1654. finally:
  1655. shutil.rmtree(project_dir)