project_test.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928
  1. # encoding: utf-8
  2. from __future__ import absolute_import
  3. from __future__ import unicode_literals
  4. import datetime
  5. import os
  6. import tempfile
  7. import docker
  8. import pytest
  9. from docker.errors import NotFound
  10. from .. import mock
  11. from .. import unittest
  12. from ..helpers import BUSYBOX_IMAGE_WITH_TAG
  13. from compose.config import ConfigurationError
  14. from compose.config.config import Config
  15. from compose.config.types import VolumeFromSpec
  16. from compose.const import COMPOSEFILE_V1 as V1
  17. from compose.const import COMPOSEFILE_V2_0 as V2_0
  18. from compose.const import COMPOSEFILE_V2_4 as V2_4
  19. from compose.const import COMPOSEFILE_V3_7 as V3_7
  20. from compose.const import DEFAULT_TIMEOUT
  21. from compose.const import LABEL_SERVICE
  22. from compose.container import Container
  23. from compose.errors import OperationFailedError
  24. from compose.project import get_secrets
  25. from compose.project import NoSuchService
  26. from compose.project import Project
  27. from compose.project import ProjectError
  28. from compose.service import ImageType
  29. from compose.service import Service
  30. class ProjectTest(unittest.TestCase):
  31. def setUp(self):
  32. self.mock_client = mock.create_autospec(docker.APIClient)
  33. self.mock_client._general_configs = {}
  34. self.mock_client.api_version = docker.constants.DEFAULT_DOCKER_API_VERSION
  35. def test_from_config_v1(self):
  36. config = Config(
  37. version=V1,
  38. services=[
  39. {
  40. 'name': 'web',
  41. 'image': BUSYBOX_IMAGE_WITH_TAG,
  42. },
  43. {
  44. 'name': 'db',
  45. 'image': BUSYBOX_IMAGE_WITH_TAG,
  46. },
  47. ],
  48. networks=None,
  49. volumes=None,
  50. secrets=None,
  51. configs=None,
  52. )
  53. project = Project.from_config(
  54. name='composetest',
  55. config_data=config,
  56. client=None,
  57. )
  58. assert len(project.services) == 2
  59. assert project.get_service('web').name == 'web'
  60. assert project.get_service('web').options['image'] == BUSYBOX_IMAGE_WITH_TAG
  61. assert project.get_service('db').name == 'db'
  62. assert project.get_service('db').options['image'] == BUSYBOX_IMAGE_WITH_TAG
  63. assert not project.networks.use_networking
  64. @mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
  65. def test_from_config_v2(self):
  66. config = Config(
  67. version=V2_0,
  68. services=[
  69. {
  70. 'name': 'web',
  71. 'image': BUSYBOX_IMAGE_WITH_TAG,
  72. },
  73. {
  74. 'name': 'db',
  75. 'image': BUSYBOX_IMAGE_WITH_TAG,
  76. },
  77. ],
  78. networks=None,
  79. volumes=None,
  80. secrets=None,
  81. configs=None,
  82. )
  83. project = Project.from_config('composetest', config, None)
  84. assert len(project.services) == 2
  85. assert project.networks.use_networking
  86. def test_get_service(self):
  87. web = Service(
  88. project='composetest',
  89. name='web',
  90. client=None,
  91. image=BUSYBOX_IMAGE_WITH_TAG,
  92. )
  93. project = Project('test', [web], None)
  94. assert project.get_service('web') == web
  95. def test_get_services_returns_all_services_without_args(self):
  96. web = Service(
  97. project='composetest',
  98. name='web',
  99. image='foo',
  100. )
  101. console = Service(
  102. project='composetest',
  103. name='console',
  104. image='foo',
  105. )
  106. project = Project('test', [web, console], None)
  107. assert project.get_services() == [web, console]
  108. def test_get_services_returns_listed_services_with_args(self):
  109. web = Service(
  110. project='composetest',
  111. name='web',
  112. image='foo',
  113. )
  114. console = Service(
  115. project='composetest',
  116. name='console',
  117. image='foo',
  118. )
  119. project = Project('test', [web, console], None)
  120. assert project.get_services(['console']) == [console]
  121. def test_get_services_with_include_links(self):
  122. db = Service(
  123. project='composetest',
  124. name='db',
  125. image='foo',
  126. )
  127. web = Service(
  128. project='composetest',
  129. name='web',
  130. image='foo',
  131. links=[(db, 'database')]
  132. )
  133. cache = Service(
  134. project='composetest',
  135. name='cache',
  136. image='foo'
  137. )
  138. console = Service(
  139. project='composetest',
  140. name='console',
  141. image='foo',
  142. links=[(web, 'web')]
  143. )
  144. project = Project('test', [web, db, cache, console], None)
  145. assert project.get_services(['console'], include_deps=True) == [db, web, console]
  146. def test_get_services_removes_duplicates_following_links(self):
  147. db = Service(
  148. project='composetest',
  149. name='db',
  150. image='foo',
  151. )
  152. web = Service(
  153. project='composetest',
  154. name='web',
  155. image='foo',
  156. links=[(db, 'database')]
  157. )
  158. project = Project('test', [web, db], None)
  159. assert project.get_services(['web', 'db'], include_deps=True) == [db, web]
  160. def test_use_volumes_from_container(self):
  161. container_id = 'aabbccddee'
  162. container_dict = dict(Name='aaa', Id=container_id)
  163. self.mock_client.inspect_container.return_value = container_dict
  164. project = Project.from_config(
  165. name='test',
  166. client=self.mock_client,
  167. config_data=Config(
  168. version=V2_0,
  169. services=[{
  170. 'name': 'test',
  171. 'image': BUSYBOX_IMAGE_WITH_TAG,
  172. 'volumes_from': [VolumeFromSpec('aaa', 'rw', 'container')]
  173. }],
  174. networks=None,
  175. volumes=None,
  176. secrets=None,
  177. configs=None,
  178. ),
  179. )
  180. assert project.get_service('test')._get_volumes_from() == [container_id + ":rw"]
  181. def test_use_volumes_from_service_no_container(self):
  182. container_name = 'test_vol_1'
  183. self.mock_client.containers.return_value = [
  184. {
  185. "Name": container_name,
  186. "Names": [container_name],
  187. "Id": container_name,
  188. "Image": BUSYBOX_IMAGE_WITH_TAG
  189. }
  190. ]
  191. project = Project.from_config(
  192. name='test',
  193. client=self.mock_client,
  194. config_data=Config(
  195. version=V2_0,
  196. services=[
  197. {
  198. 'name': 'vol',
  199. 'image': BUSYBOX_IMAGE_WITH_TAG
  200. },
  201. {
  202. 'name': 'test',
  203. 'image': BUSYBOX_IMAGE_WITH_TAG,
  204. 'volumes_from': [VolumeFromSpec('vol', 'rw', 'service')]
  205. }
  206. ],
  207. networks=None,
  208. volumes=None,
  209. secrets=None,
  210. configs=None,
  211. ),
  212. )
  213. assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"]
  214. @mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
  215. def test_use_volumes_from_service_container(self):
  216. container_ids = ['aabbccddee', '12345']
  217. project = Project.from_config(
  218. name='test',
  219. client=None,
  220. config_data=Config(
  221. version=V2_0,
  222. services=[
  223. {
  224. 'name': 'vol',
  225. 'image': BUSYBOX_IMAGE_WITH_TAG
  226. },
  227. {
  228. 'name': 'test',
  229. 'image': BUSYBOX_IMAGE_WITH_TAG,
  230. 'volumes_from': [VolumeFromSpec('vol', 'rw', 'service')]
  231. }
  232. ],
  233. networks=None,
  234. volumes=None,
  235. secrets=None,
  236. configs=None,
  237. ),
  238. )
  239. with mock.patch.object(Service, 'containers') as mock_return:
  240. mock_return.return_value = [
  241. mock.Mock(id=container_id, spec=Container)
  242. for container_id in container_ids]
  243. assert (
  244. project.get_service('test')._get_volumes_from() ==
  245. [container_ids[0] + ':rw']
  246. )
  247. def test_events_legacy(self):
  248. services = [Service(name='web'), Service(name='db')]
  249. project = Project('test', services, self.mock_client)
  250. self.mock_client.api_version = '1.21'
  251. self.mock_client.events.return_value = iter([
  252. {
  253. 'status': 'create',
  254. 'from': 'example/image',
  255. 'id': 'abcde',
  256. 'time': 1420092061,
  257. 'timeNano': 14200920610000002000,
  258. },
  259. {
  260. 'status': 'attach',
  261. 'from': 'example/image',
  262. 'id': 'abcde',
  263. 'time': 1420092061,
  264. 'timeNano': 14200920610000003000,
  265. },
  266. {
  267. 'status': 'create',
  268. 'from': 'example/other',
  269. 'id': 'bdbdbd',
  270. 'time': 1420092061,
  271. 'timeNano': 14200920610000005000,
  272. },
  273. {
  274. 'status': 'create',
  275. 'from': 'example/db',
  276. 'id': 'ababa',
  277. 'time': 1420092061,
  278. 'timeNano': 14200920610000004000,
  279. },
  280. {
  281. 'status': 'destroy',
  282. 'from': 'example/db',
  283. 'id': 'eeeee',
  284. 'time': 1420092061,
  285. 'timeNano': 14200920610000004000,
  286. },
  287. ])
  288. def dt_with_microseconds(dt, us):
  289. return datetime.datetime.fromtimestamp(dt).replace(microsecond=us)
  290. def get_container(cid):
  291. if cid == 'eeeee':
  292. raise NotFound(None, None, "oops")
  293. if cid == 'abcde':
  294. name = 'web'
  295. labels = {LABEL_SERVICE: name}
  296. elif cid == 'ababa':
  297. name = 'db'
  298. labels = {LABEL_SERVICE: name}
  299. else:
  300. labels = {}
  301. name = ''
  302. return {
  303. 'Id': cid,
  304. 'Config': {'Labels': labels},
  305. 'Name': '/project_%s_1' % name,
  306. }
  307. self.mock_client.inspect_container.side_effect = get_container
  308. events = project.events()
  309. events_list = list(events)
  310. # Assert the return value is a generator
  311. assert not list(events)
  312. assert events_list == [
  313. {
  314. 'type': 'container',
  315. 'service': 'web',
  316. 'action': 'create',
  317. 'id': 'abcde',
  318. 'attributes': {
  319. 'name': 'project_web_1',
  320. 'image': 'example/image',
  321. },
  322. 'time': dt_with_microseconds(1420092061, 2),
  323. 'container': Container(None, {'Id': 'abcde'}),
  324. },
  325. {
  326. 'type': 'container',
  327. 'service': 'web',
  328. 'action': 'attach',
  329. 'id': 'abcde',
  330. 'attributes': {
  331. 'name': 'project_web_1',
  332. 'image': 'example/image',
  333. },
  334. 'time': dt_with_microseconds(1420092061, 3),
  335. 'container': Container(None, {'Id': 'abcde'}),
  336. },
  337. {
  338. 'type': 'container',
  339. 'service': 'db',
  340. 'action': 'create',
  341. 'id': 'ababa',
  342. 'attributes': {
  343. 'name': 'project_db_1',
  344. 'image': 'example/db',
  345. },
  346. 'time': dt_with_microseconds(1420092061, 4),
  347. 'container': Container(None, {'Id': 'ababa'}),
  348. },
  349. ]
  350. def test_events(self):
  351. services = [Service(name='web'), Service(name='db')]
  352. project = Project('test', services, self.mock_client)
  353. self.mock_client.api_version = '1.35'
  354. self.mock_client.events.return_value = iter([
  355. {
  356. 'status': 'create',
  357. 'from': 'example/image',
  358. 'Type': 'container',
  359. 'Actor': {
  360. 'ID': 'abcde',
  361. 'Attributes': {
  362. 'com.docker.compose.project': 'test',
  363. 'com.docker.compose.service': 'web',
  364. 'image': 'example/image',
  365. 'name': 'test_web_1',
  366. }
  367. },
  368. 'id': 'abcde',
  369. 'time': 1420092061,
  370. 'timeNano': 14200920610000002000,
  371. },
  372. {
  373. 'status': 'attach',
  374. 'from': 'example/image',
  375. 'Type': 'container',
  376. 'Actor': {
  377. 'ID': 'abcde',
  378. 'Attributes': {
  379. 'com.docker.compose.project': 'test',
  380. 'com.docker.compose.service': 'web',
  381. 'image': 'example/image',
  382. 'name': 'test_web_1',
  383. }
  384. },
  385. 'id': 'abcde',
  386. 'time': 1420092061,
  387. 'timeNano': 14200920610000003000,
  388. },
  389. {
  390. 'status': 'create',
  391. 'from': 'example/other',
  392. 'Type': 'container',
  393. 'Actor': {
  394. 'ID': 'bdbdbd',
  395. 'Attributes': {
  396. 'image': 'example/other',
  397. 'name': 'shrewd_einstein',
  398. }
  399. },
  400. 'id': 'bdbdbd',
  401. 'time': 1420092061,
  402. 'timeNano': 14200920610000005000,
  403. },
  404. {
  405. 'status': 'create',
  406. 'from': 'example/db',
  407. 'Type': 'container',
  408. 'Actor': {
  409. 'ID': 'ababa',
  410. 'Attributes': {
  411. 'com.docker.compose.project': 'test',
  412. 'com.docker.compose.service': 'db',
  413. 'image': 'example/db',
  414. 'name': 'test_db_1',
  415. }
  416. },
  417. 'id': 'ababa',
  418. 'time': 1420092061,
  419. 'timeNano': 14200920610000004000,
  420. },
  421. {
  422. 'status': 'destroy',
  423. 'from': 'example/db',
  424. 'Type': 'container',
  425. 'Actor': {
  426. 'ID': 'eeeee',
  427. 'Attributes': {
  428. 'com.docker.compose.project': 'test',
  429. 'com.docker.compose.service': 'db',
  430. 'image': 'example/db',
  431. 'name': 'test_db_1',
  432. }
  433. },
  434. 'id': 'eeeee',
  435. 'time': 1420092061,
  436. 'timeNano': 14200920610000004000,
  437. },
  438. ])
  439. def dt_with_microseconds(dt, us):
  440. return datetime.datetime.fromtimestamp(dt).replace(microsecond=us)
  441. def get_container(cid):
  442. if cid == 'eeeee':
  443. raise NotFound(None, None, "oops")
  444. if cid == 'abcde':
  445. name = 'web'
  446. labels = {LABEL_SERVICE: name}
  447. elif cid == 'ababa':
  448. name = 'db'
  449. labels = {LABEL_SERVICE: name}
  450. else:
  451. labels = {}
  452. name = ''
  453. return {
  454. 'Id': cid,
  455. 'Config': {'Labels': labels},
  456. 'Name': '/project_%s_1' % name,
  457. }
  458. self.mock_client.inspect_container.side_effect = get_container
  459. events = project.events()
  460. events_list = list(events)
  461. # Assert the return value is a generator
  462. assert not list(events)
  463. assert events_list == [
  464. {
  465. 'type': 'container',
  466. 'service': 'web',
  467. 'action': 'create',
  468. 'id': 'abcde',
  469. 'attributes': {
  470. 'name': 'test_web_1',
  471. 'image': 'example/image',
  472. },
  473. 'time': dt_with_microseconds(1420092061, 2),
  474. 'container': Container(None, get_container('abcde')),
  475. },
  476. {
  477. 'type': 'container',
  478. 'service': 'web',
  479. 'action': 'attach',
  480. 'id': 'abcde',
  481. 'attributes': {
  482. 'name': 'test_web_1',
  483. 'image': 'example/image',
  484. },
  485. 'time': dt_with_microseconds(1420092061, 3),
  486. 'container': Container(None, get_container('abcde')),
  487. },
  488. {
  489. 'type': 'container',
  490. 'service': 'db',
  491. 'action': 'create',
  492. 'id': 'ababa',
  493. 'attributes': {
  494. 'name': 'test_db_1',
  495. 'image': 'example/db',
  496. },
  497. 'time': dt_with_microseconds(1420092061, 4),
  498. 'container': Container(None, get_container('ababa')),
  499. },
  500. {
  501. 'type': 'container',
  502. 'service': 'db',
  503. 'action': 'destroy',
  504. 'id': 'eeeee',
  505. 'attributes': {
  506. 'name': 'test_db_1',
  507. 'image': 'example/db',
  508. },
  509. 'time': dt_with_microseconds(1420092061, 4),
  510. 'container': None,
  511. },
  512. ]
  513. def test_net_unset(self):
  514. project = Project.from_config(
  515. name='test',
  516. client=self.mock_client,
  517. config_data=Config(
  518. version=V1,
  519. services=[
  520. {
  521. 'name': 'test',
  522. 'image': BUSYBOX_IMAGE_WITH_TAG,
  523. }
  524. ],
  525. networks=None,
  526. volumes=None,
  527. secrets=None,
  528. configs=None,
  529. ),
  530. )
  531. service = project.get_service('test')
  532. assert service.network_mode.id is None
  533. assert 'NetworkMode' not in service._get_container_host_config({})
  534. def test_use_net_from_container(self):
  535. container_id = 'aabbccddee'
  536. container_dict = dict(Name='aaa', Id=container_id)
  537. self.mock_client.inspect_container.return_value = container_dict
  538. project = Project.from_config(
  539. name='test',
  540. client=self.mock_client,
  541. config_data=Config(
  542. version=V2_0,
  543. services=[
  544. {
  545. 'name': 'test',
  546. 'image': BUSYBOX_IMAGE_WITH_TAG,
  547. 'network_mode': 'container:aaa'
  548. },
  549. ],
  550. networks=None,
  551. volumes=None,
  552. secrets=None,
  553. configs=None,
  554. ),
  555. )
  556. service = project.get_service('test')
  557. assert service.network_mode.mode == 'container:' + container_id
  558. def test_use_net_from_service(self):
  559. container_name = 'test_aaa_1'
  560. self.mock_client.containers.return_value = [
  561. {
  562. "Name": container_name,
  563. "Names": [container_name],
  564. "Id": container_name,
  565. "Image": BUSYBOX_IMAGE_WITH_TAG
  566. }
  567. ]
  568. project = Project.from_config(
  569. name='test',
  570. client=self.mock_client,
  571. config_data=Config(
  572. version=V2_0,
  573. services=[
  574. {
  575. 'name': 'aaa',
  576. 'image': BUSYBOX_IMAGE_WITH_TAG
  577. },
  578. {
  579. 'name': 'test',
  580. 'image': BUSYBOX_IMAGE_WITH_TAG,
  581. 'network_mode': 'service:aaa'
  582. },
  583. ],
  584. networks=None,
  585. volumes=None,
  586. secrets=None,
  587. configs=None,
  588. ),
  589. )
  590. service = project.get_service('test')
  591. assert service.network_mode.mode == 'container:' + container_name
  592. def test_uses_default_network_true(self):
  593. project = Project.from_config(
  594. name='test',
  595. client=self.mock_client,
  596. config_data=Config(
  597. version=V2_0,
  598. services=[
  599. {
  600. 'name': 'foo',
  601. 'image': BUSYBOX_IMAGE_WITH_TAG
  602. },
  603. ],
  604. networks=None,
  605. volumes=None,
  606. secrets=None,
  607. configs=None,
  608. ),
  609. )
  610. assert 'default' in project.networks.networks
  611. def test_uses_default_network_false(self):
  612. project = Project.from_config(
  613. name='test',
  614. client=self.mock_client,
  615. config_data=Config(
  616. version=V2_0,
  617. services=[
  618. {
  619. 'name': 'foo',
  620. 'image': BUSYBOX_IMAGE_WITH_TAG,
  621. 'networks': {'custom': None}
  622. },
  623. ],
  624. networks={'custom': {}},
  625. volumes=None,
  626. secrets=None,
  627. configs=None,
  628. ),
  629. )
  630. assert 'default' not in project.networks.networks
  631. def test_container_without_name(self):
  632. self.mock_client.containers.return_value = [
  633. {'Image': BUSYBOX_IMAGE_WITH_TAG, 'Id': '1', 'Name': '1'},
  634. {'Image': BUSYBOX_IMAGE_WITH_TAG, 'Id': '2', 'Name': None},
  635. {'Image': BUSYBOX_IMAGE_WITH_TAG, 'Id': '3'},
  636. ]
  637. self.mock_client.inspect_container.return_value = {
  638. 'Id': '1',
  639. 'Config': {
  640. 'Labels': {
  641. LABEL_SERVICE: 'web',
  642. },
  643. },
  644. }
  645. project = Project.from_config(
  646. name='test',
  647. client=self.mock_client,
  648. config_data=Config(
  649. version=V2_0,
  650. services=[{
  651. 'name': 'web',
  652. 'image': BUSYBOX_IMAGE_WITH_TAG,
  653. }],
  654. networks=None,
  655. volumes=None,
  656. secrets=None,
  657. configs=None,
  658. ),
  659. )
  660. assert [c.id for c in project.containers()] == ['1']
  661. def test_down_with_no_resources(self):
  662. project = Project.from_config(
  663. name='test',
  664. client=self.mock_client,
  665. config_data=Config(
  666. version=V2_0,
  667. services=[{
  668. 'name': 'web',
  669. 'image': BUSYBOX_IMAGE_WITH_TAG,
  670. }],
  671. networks={'default': {}},
  672. volumes={'data': {}},
  673. secrets=None,
  674. configs=None,
  675. ),
  676. )
  677. self.mock_client.remove_network.side_effect = NotFound(None, None, 'oops')
  678. self.mock_client.remove_volume.side_effect = NotFound(None, None, 'oops')
  679. project.down(ImageType.all, True)
  680. self.mock_client.remove_image.assert_called_once_with(BUSYBOX_IMAGE_WITH_TAG)
  681. def test_no_warning_on_stop(self):
  682. self.mock_client.info.return_value = {'Swarm': {'LocalNodeState': 'active'}}
  683. project = Project('composetest', [], self.mock_client)
  684. with mock.patch('compose.project.log') as fake_log:
  685. project.stop()
  686. assert fake_log.warn.call_count == 0
  687. def test_no_warning_in_normal_mode(self):
  688. self.mock_client.info.return_value = {'Swarm': {'LocalNodeState': 'inactive'}}
  689. project = Project('composetest', [], self.mock_client)
  690. with mock.patch('compose.project.log') as fake_log:
  691. project.up()
  692. assert fake_log.warn.call_count == 0
  693. def test_no_warning_with_no_swarm_info(self):
  694. self.mock_client.info.return_value = {}
  695. project = Project('composetest', [], self.mock_client)
  696. with mock.patch('compose.project.log') as fake_log:
  697. project.up()
  698. assert fake_log.warn.call_count == 0
  699. def test_no_such_service_unicode(self):
  700. assert NoSuchService('十六夜 咲夜'.encode('utf-8')).msg == 'No such service: 十六夜 咲夜'
  701. assert NoSuchService('十六夜 咲夜').msg == 'No such service: 十六夜 咲夜'
  702. def test_project_platform_value(self):
  703. service_config = {
  704. 'name': 'web',
  705. 'image': BUSYBOX_IMAGE_WITH_TAG,
  706. }
  707. config_data = Config(
  708. version=V2_4, services=[service_config], networks={}, volumes={}, secrets=None, configs=None
  709. )
  710. project = Project.from_config(name='test', client=self.mock_client, config_data=config_data)
  711. assert project.get_service('web').platform is None
  712. project = Project.from_config(
  713. name='test', client=self.mock_client, config_data=config_data, default_platform='windows'
  714. )
  715. assert project.get_service('web').platform == 'windows'
  716. service_config['platform'] = 'linux/s390x'
  717. project = Project.from_config(name='test', client=self.mock_client, config_data=config_data)
  718. assert project.get_service('web').platform == 'linux/s390x'
  719. project = Project.from_config(
  720. name='test', client=self.mock_client, config_data=config_data, default_platform='windows'
  721. )
  722. assert project.get_service('web').platform == 'linux/s390x'
  723. def test_build_container_operation_with_timeout_func_does_not_mutate_options_with_timeout(self):
  724. config_data = Config(
  725. version=V3_7,
  726. services=[
  727. {'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG},
  728. {'name': 'db', 'image': BUSYBOX_IMAGE_WITH_TAG, 'stop_grace_period': '1s'},
  729. ],
  730. networks={}, volumes={}, secrets=None, configs=None,
  731. )
  732. project = Project.from_config(name='test', client=self.mock_client, config_data=config_data)
  733. stop_op = project.build_container_operation_with_timeout_func('stop', options={})
  734. web_container = mock.create_autospec(Container, service='web')
  735. db_container = mock.create_autospec(Container, service='db')
  736. # `stop_grace_period` is not set to 'web' service,
  737. # then it is stopped with the default timeout.
  738. stop_op(web_container)
  739. web_container.stop.assert_called_once_with(timeout=DEFAULT_TIMEOUT)
  740. # `stop_grace_period` is set to 'db' service,
  741. # then it is stopped with the specified timeout and
  742. # the value is not overridden by the previous function call.
  743. stop_op(db_container)
  744. db_container.stop.assert_called_once_with(timeout=1)
  745. @mock.patch('compose.parallel.ParallelStreamWriter._write_noansi')
  746. def test_error_parallel_pull(self, mock_write):
  747. project = Project.from_config(
  748. name='test',
  749. client=self.mock_client,
  750. config_data=Config(
  751. version=V2_0,
  752. services=[{
  753. 'name': 'web',
  754. 'image': BUSYBOX_IMAGE_WITH_TAG,
  755. }],
  756. networks=None,
  757. volumes=None,
  758. secrets=None,
  759. configs=None,
  760. ),
  761. )
  762. self.mock_client.pull.side_effect = OperationFailedError('pull error')
  763. with pytest.raises(ProjectError):
  764. project.pull(parallel_pull=True)
  765. self.mock_client.pull.side_effect = OperationFailedError(b'pull error')
  766. with pytest.raises(ProjectError):
  767. project.pull(parallel_pull=True)
  768. def test_avoid_multiple_push(self):
  769. service_config_latest = {'image': 'busybox:latest', 'build': '.'}
  770. service_config_default = {'image': 'busybox', 'build': '.'}
  771. service_config_sha = {
  772. 'image': 'busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d',
  773. 'build': '.'
  774. }
  775. svc1 = Service('busy1', **service_config_latest)
  776. svc1_1 = Service('busy11', **service_config_latest)
  777. svc2 = Service('busy2', **service_config_default)
  778. svc2_1 = Service('busy21', **service_config_default)
  779. svc3 = Service('busy3', **service_config_sha)
  780. svc3_1 = Service('busy31', **service_config_sha)
  781. project = Project(
  782. 'composetest', [svc1, svc1_1, svc2, svc2_1, svc3, svc3_1], self.mock_client
  783. )
  784. with mock.patch('compose.service.Service.push') as fake_push:
  785. project.push()
  786. assert fake_push.call_count == 2
  787. def test_get_secrets_no_secret_def(self):
  788. service = 'foo'
  789. secret_source = 'bar'
  790. secret_defs = mock.Mock()
  791. secret_defs.get.return_value = None
  792. secret = mock.Mock(source=secret_source)
  793. with self.assertRaises(ConfigurationError):
  794. get_secrets(service, [secret], secret_defs)
  795. def test_get_secrets_external_warning(self):
  796. service = 'foo'
  797. secret_source = 'bar'
  798. secret_def = mock.Mock()
  799. secret_def.get.return_value = True
  800. secret_defs = mock.Mock()
  801. secret_defs.get.side_effect = secret_def
  802. secret = mock.Mock(source=secret_source)
  803. with mock.patch('compose.project.log') as mock_log:
  804. get_secrets(service, [secret], secret_defs)
  805. mock_log.warning.assert_called_with("Service \"{service}\" uses secret \"{secret}\" "
  806. "which is external. External secrets are not available"
  807. " to containers created by docker-compose."
  808. .format(service=service, secret=secret_source))
  809. def test_get_secrets_uid_gid_mode_warning(self):
  810. service = 'foo'
  811. secret_source = 'bar'
  812. fd, filename_path = tempfile.mkstemp()
  813. os.close(fd)
  814. self.addCleanup(os.remove, filename_path)
  815. def mock_get(key):
  816. return {'external': False, 'file': filename_path}[key]
  817. secret_def = mock.MagicMock()
  818. secret_def.get = mock.MagicMock(side_effect=mock_get)
  819. secret_defs = mock.Mock()
  820. secret_defs.get.return_value = secret_def
  821. secret = mock.Mock(uid=True, gid=True, mode=True, source=secret_source)
  822. with mock.patch('compose.project.log') as mock_log:
  823. get_secrets(service, [secret], secret_defs)
  824. mock_log.warning.assert_called_with("Service \"{service}\" uses secret \"{secret}\" with uid, "
  825. "gid, or mode. These fields are not supported by this "
  826. "implementation of the Compose file"
  827. .format(service=service, secret=secret_source))
  828. def test_get_secrets_secret_file_warning(self):
  829. service = 'foo'
  830. secret_source = 'bar'
  831. not_a_path = 'NOT_A_PATH'
  832. def mock_get(key):
  833. return {'external': False, 'file': not_a_path}[key]
  834. secret_def = mock.MagicMock()
  835. secret_def.get = mock.MagicMock(side_effect=mock_get)
  836. secret_defs = mock.Mock()
  837. secret_defs.get.return_value = secret_def
  838. secret = mock.Mock(uid=False, gid=False, mode=False, source=secret_source)
  839. with mock.patch('compose.project.log') as mock_log:
  840. get_secrets(service, [secret], secret_defs)
  841. mock_log.warning.assert_called_with("Service \"{service}\" uses an undefined secret file "
  842. "\"{secret_file}\", the following file should be created "
  843. "\"{secret_file}\""
  844. .format(service=service, secret_file=not_a_path))