project_test.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. # encoding: utf-8
  2. from __future__ import absolute_import
  3. from __future__ import unicode_literals
  4. import datetime
  5. import docker
  6. import pytest
  7. from docker.errors import NotFound
  8. from .. import mock
  9. from .. import unittest
  10. from compose.config.config import Config
  11. from compose.config.types import VolumeFromSpec
  12. from compose.const import COMPOSEFILE_V1 as V1
  13. from compose.const import COMPOSEFILE_V2_0 as V2_0
  14. from compose.const import COMPOSEFILE_V2_4 as V2_4
  15. from compose.const import LABEL_SERVICE
  16. from compose.container import Container
  17. from compose.errors import OperationFailedError
  18. from compose.project import NoSuchService
  19. from compose.project import Project
  20. from compose.project import ProjectError
  21. from compose.service import ImageType
  22. from compose.service import Service
  23. class ProjectTest(unittest.TestCase):
  24. def setUp(self):
  25. self.mock_client = mock.create_autospec(docker.APIClient)
  26. self.mock_client._general_configs = {}
  27. self.mock_client.api_version = docker.constants.DEFAULT_DOCKER_API_VERSION
  28. def test_from_config_v1(self):
  29. config = Config(
  30. version=V1,
  31. services=[
  32. {
  33. 'name': 'web',
  34. 'image': 'busybox:latest',
  35. },
  36. {
  37. 'name': 'db',
  38. 'image': 'busybox:latest',
  39. },
  40. ],
  41. networks=None,
  42. volumes=None,
  43. secrets=None,
  44. configs=None,
  45. )
  46. project = Project.from_config(
  47. name='composetest',
  48. config_data=config,
  49. client=None,
  50. )
  51. assert len(project.services) == 2
  52. assert project.get_service('web').name == 'web'
  53. assert project.get_service('web').options['image'] == 'busybox:latest'
  54. assert project.get_service('db').name == 'db'
  55. assert project.get_service('db').options['image'] == 'busybox:latest'
  56. assert not project.networks.use_networking
  57. @mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
  58. def test_from_config_v2(self):
  59. config = Config(
  60. version=V2_0,
  61. services=[
  62. {
  63. 'name': 'web',
  64. 'image': 'busybox:latest',
  65. },
  66. {
  67. 'name': 'db',
  68. 'image': 'busybox:latest',
  69. },
  70. ],
  71. networks=None,
  72. volumes=None,
  73. secrets=None,
  74. configs=None,
  75. )
  76. project = Project.from_config('composetest', config, None)
  77. assert len(project.services) == 2
  78. assert project.networks.use_networking
  79. def test_get_service(self):
  80. web = Service(
  81. project='composetest',
  82. name='web',
  83. client=None,
  84. image="busybox:latest",
  85. )
  86. project = Project('test', [web], None)
  87. assert project.get_service('web') == web
  88. def test_get_services_returns_all_services_without_args(self):
  89. web = Service(
  90. project='composetest',
  91. name='web',
  92. image='foo',
  93. )
  94. console = Service(
  95. project='composetest',
  96. name='console',
  97. image='foo',
  98. )
  99. project = Project('test', [web, console], None)
  100. assert project.get_services() == [web, console]
  101. def test_get_services_returns_listed_services_with_args(self):
  102. web = Service(
  103. project='composetest',
  104. name='web',
  105. image='foo',
  106. )
  107. console = Service(
  108. project='composetest',
  109. name='console',
  110. image='foo',
  111. )
  112. project = Project('test', [web, console], None)
  113. assert project.get_services(['console']) == [console]
  114. def test_get_services_with_include_links(self):
  115. db = Service(
  116. project='composetest',
  117. name='db',
  118. image='foo',
  119. )
  120. web = Service(
  121. project='composetest',
  122. name='web',
  123. image='foo',
  124. links=[(db, 'database')]
  125. )
  126. cache = Service(
  127. project='composetest',
  128. name='cache',
  129. image='foo'
  130. )
  131. console = Service(
  132. project='composetest',
  133. name='console',
  134. image='foo',
  135. links=[(web, 'web')]
  136. )
  137. project = Project('test', [web, db, cache, console], None)
  138. assert project.get_services(['console'], include_deps=True) == [db, web, console]
  139. def test_get_services_removes_duplicates_following_links(self):
  140. db = Service(
  141. project='composetest',
  142. name='db',
  143. image='foo',
  144. )
  145. web = Service(
  146. project='composetest',
  147. name='web',
  148. image='foo',
  149. links=[(db, 'database')]
  150. )
  151. project = Project('test', [web, db], None)
  152. assert project.get_services(['web', 'db'], include_deps=True) == [db, web]
  153. def test_use_volumes_from_container(self):
  154. container_id = 'aabbccddee'
  155. container_dict = dict(Name='aaa', Id=container_id)
  156. self.mock_client.inspect_container.return_value = container_dict
  157. project = Project.from_config(
  158. name='test',
  159. client=self.mock_client,
  160. config_data=Config(
  161. version=V2_0,
  162. services=[{
  163. 'name': 'test',
  164. 'image': 'busybox:latest',
  165. 'volumes_from': [VolumeFromSpec('aaa', 'rw', 'container')]
  166. }],
  167. networks=None,
  168. volumes=None,
  169. secrets=None,
  170. configs=None,
  171. ),
  172. )
  173. assert project.get_service('test')._get_volumes_from() == [container_id + ":rw"]
  174. def test_use_volumes_from_service_no_container(self):
  175. container_name = 'test_vol_1'
  176. self.mock_client.containers.return_value = [
  177. {
  178. "Name": container_name,
  179. "Names": [container_name],
  180. "Id": container_name,
  181. "Image": 'busybox:latest'
  182. }
  183. ]
  184. project = Project.from_config(
  185. name='test',
  186. client=self.mock_client,
  187. config_data=Config(
  188. version=V2_0,
  189. services=[
  190. {
  191. 'name': 'vol',
  192. 'image': 'busybox:latest'
  193. },
  194. {
  195. 'name': 'test',
  196. 'image': 'busybox:latest',
  197. 'volumes_from': [VolumeFromSpec('vol', 'rw', 'service')]
  198. }
  199. ],
  200. networks=None,
  201. volumes=None,
  202. secrets=None,
  203. configs=None,
  204. ),
  205. )
  206. assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"]
  207. @mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
  208. def test_use_volumes_from_service_container(self):
  209. container_ids = ['aabbccddee', '12345']
  210. project = Project.from_config(
  211. name='test',
  212. client=None,
  213. config_data=Config(
  214. version=V2_0,
  215. services=[
  216. {
  217. 'name': 'vol',
  218. 'image': 'busybox:latest'
  219. },
  220. {
  221. 'name': 'test',
  222. 'image': 'busybox:latest',
  223. 'volumes_from': [VolumeFromSpec('vol', 'rw', 'service')]
  224. }
  225. ],
  226. networks=None,
  227. volumes=None,
  228. secrets=None,
  229. configs=None,
  230. ),
  231. )
  232. with mock.patch.object(Service, 'containers') as mock_return:
  233. mock_return.return_value = [
  234. mock.Mock(id=container_id, spec=Container)
  235. for container_id in container_ids]
  236. assert (
  237. project.get_service('test')._get_volumes_from() ==
  238. [container_ids[0] + ':rw']
  239. )
  240. def test_events(self):
  241. services = [Service(name='web'), Service(name='db')]
  242. project = Project('test', services, self.mock_client)
  243. self.mock_client.events.return_value = iter([
  244. {
  245. 'status': 'create',
  246. 'from': 'example/image',
  247. 'id': 'abcde',
  248. 'time': 1420092061,
  249. 'timeNano': 14200920610000002000,
  250. },
  251. {
  252. 'status': 'attach',
  253. 'from': 'example/image',
  254. 'id': 'abcde',
  255. 'time': 1420092061,
  256. 'timeNano': 14200920610000003000,
  257. },
  258. {
  259. 'status': 'create',
  260. 'from': 'example/other',
  261. 'id': 'bdbdbd',
  262. 'time': 1420092061,
  263. 'timeNano': 14200920610000005000,
  264. },
  265. {
  266. 'status': 'create',
  267. 'from': 'example/db',
  268. 'id': 'ababa',
  269. 'time': 1420092061,
  270. 'timeNano': 14200920610000004000,
  271. },
  272. {
  273. 'status': 'destroy',
  274. 'from': 'example/db',
  275. 'id': 'eeeee',
  276. 'time': 1420092061,
  277. 'timeNano': 14200920610000004000,
  278. },
  279. ])
  280. def dt_with_microseconds(dt, us):
  281. return datetime.datetime.fromtimestamp(dt).replace(microsecond=us)
  282. def get_container(cid):
  283. if cid == 'eeeee':
  284. raise NotFound(None, None, "oops")
  285. if cid == 'abcde':
  286. name = 'web'
  287. labels = {LABEL_SERVICE: name}
  288. elif cid == 'ababa':
  289. name = 'db'
  290. labels = {LABEL_SERVICE: name}
  291. else:
  292. labels = {}
  293. name = ''
  294. return {
  295. 'Id': cid,
  296. 'Config': {'Labels': labels},
  297. 'Name': '/project_%s_1' % name,
  298. }
  299. self.mock_client.inspect_container.side_effect = get_container
  300. events = project.events()
  301. events_list = list(events)
  302. # Assert the return value is a generator
  303. assert not list(events)
  304. assert events_list == [
  305. {
  306. 'type': 'container',
  307. 'service': 'web',
  308. 'action': 'create',
  309. 'id': 'abcde',
  310. 'attributes': {
  311. 'name': 'project_web_1',
  312. 'image': 'example/image',
  313. },
  314. 'time': dt_with_microseconds(1420092061, 2),
  315. 'container': Container(None, {'Id': 'abcde'}),
  316. },
  317. {
  318. 'type': 'container',
  319. 'service': 'web',
  320. 'action': 'attach',
  321. 'id': 'abcde',
  322. 'attributes': {
  323. 'name': 'project_web_1',
  324. 'image': 'example/image',
  325. },
  326. 'time': dt_with_microseconds(1420092061, 3),
  327. 'container': Container(None, {'Id': 'abcde'}),
  328. },
  329. {
  330. 'type': 'container',
  331. 'service': 'db',
  332. 'action': 'create',
  333. 'id': 'ababa',
  334. 'attributes': {
  335. 'name': 'project_db_1',
  336. 'image': 'example/db',
  337. },
  338. 'time': dt_with_microseconds(1420092061, 4),
  339. 'container': Container(None, {'Id': 'ababa'}),
  340. },
  341. ]
  342. def test_net_unset(self):
  343. project = Project.from_config(
  344. name='test',
  345. client=self.mock_client,
  346. config_data=Config(
  347. version=V1,
  348. services=[
  349. {
  350. 'name': 'test',
  351. 'image': 'busybox:latest',
  352. }
  353. ],
  354. networks=None,
  355. volumes=None,
  356. secrets=None,
  357. configs=None,
  358. ),
  359. )
  360. service = project.get_service('test')
  361. assert service.network_mode.id is None
  362. assert 'NetworkMode' not in service._get_container_host_config({})
  363. def test_use_net_from_container(self):
  364. container_id = 'aabbccddee'
  365. container_dict = dict(Name='aaa', Id=container_id)
  366. self.mock_client.inspect_container.return_value = container_dict
  367. project = Project.from_config(
  368. name='test',
  369. client=self.mock_client,
  370. config_data=Config(
  371. version=V2_0,
  372. services=[
  373. {
  374. 'name': 'test',
  375. 'image': 'busybox:latest',
  376. 'network_mode': 'container:aaa'
  377. },
  378. ],
  379. networks=None,
  380. volumes=None,
  381. secrets=None,
  382. configs=None,
  383. ),
  384. )
  385. service = project.get_service('test')
  386. assert service.network_mode.mode == 'container:' + container_id
  387. def test_use_net_from_service(self):
  388. container_name = 'test_aaa_1'
  389. self.mock_client.containers.return_value = [
  390. {
  391. "Name": container_name,
  392. "Names": [container_name],
  393. "Id": container_name,
  394. "Image": 'busybox:latest'
  395. }
  396. ]
  397. project = Project.from_config(
  398. name='test',
  399. client=self.mock_client,
  400. config_data=Config(
  401. version=V2_0,
  402. services=[
  403. {
  404. 'name': 'aaa',
  405. 'image': 'busybox:latest'
  406. },
  407. {
  408. 'name': 'test',
  409. 'image': 'busybox:latest',
  410. 'network_mode': 'service:aaa'
  411. },
  412. ],
  413. networks=None,
  414. volumes=None,
  415. secrets=None,
  416. configs=None,
  417. ),
  418. )
  419. service = project.get_service('test')
  420. assert service.network_mode.mode == 'container:' + container_name
  421. def test_uses_default_network_true(self):
  422. project = Project.from_config(
  423. name='test',
  424. client=self.mock_client,
  425. config_data=Config(
  426. version=V2_0,
  427. services=[
  428. {
  429. 'name': 'foo',
  430. 'image': 'busybox:latest'
  431. },
  432. ],
  433. networks=None,
  434. volumes=None,
  435. secrets=None,
  436. configs=None,
  437. ),
  438. )
  439. assert 'default' in project.networks.networks
  440. def test_uses_default_network_false(self):
  441. project = Project.from_config(
  442. name='test',
  443. client=self.mock_client,
  444. config_data=Config(
  445. version=V2_0,
  446. services=[
  447. {
  448. 'name': 'foo',
  449. 'image': 'busybox:latest',
  450. 'networks': {'custom': None}
  451. },
  452. ],
  453. networks={'custom': {}},
  454. volumes=None,
  455. secrets=None,
  456. configs=None,
  457. ),
  458. )
  459. assert 'default' not in project.networks.networks
  460. def test_container_without_name(self):
  461. self.mock_client.containers.return_value = [
  462. {'Image': 'busybox:latest', 'Id': '1', 'Name': '1'},
  463. {'Image': 'busybox:latest', 'Id': '2', 'Name': None},
  464. {'Image': 'busybox:latest', 'Id': '3'},
  465. ]
  466. self.mock_client.inspect_container.return_value = {
  467. 'Id': '1',
  468. 'Config': {
  469. 'Labels': {
  470. LABEL_SERVICE: 'web',
  471. },
  472. },
  473. }
  474. project = Project.from_config(
  475. name='test',
  476. client=self.mock_client,
  477. config_data=Config(
  478. version=V2_0,
  479. services=[{
  480. 'name': 'web',
  481. 'image': 'busybox:latest',
  482. }],
  483. networks=None,
  484. volumes=None,
  485. secrets=None,
  486. configs=None,
  487. ),
  488. )
  489. assert [c.id for c in project.containers()] == ['1']
  490. def test_down_with_no_resources(self):
  491. project = Project.from_config(
  492. name='test',
  493. client=self.mock_client,
  494. config_data=Config(
  495. version=V2_0,
  496. services=[{
  497. 'name': 'web',
  498. 'image': 'busybox:latest',
  499. }],
  500. networks={'default': {}},
  501. volumes={'data': {}},
  502. secrets=None,
  503. configs=None,
  504. ),
  505. )
  506. self.mock_client.remove_network.side_effect = NotFound(None, None, 'oops')
  507. self.mock_client.remove_volume.side_effect = NotFound(None, None, 'oops')
  508. project.down(ImageType.all, True)
  509. self.mock_client.remove_image.assert_called_once_with("busybox:latest")
  510. def test_no_warning_on_stop(self):
  511. self.mock_client.info.return_value = {'Swarm': {'LocalNodeState': 'active'}}
  512. project = Project('composetest', [], self.mock_client)
  513. with mock.patch('compose.project.log') as fake_log:
  514. project.stop()
  515. assert fake_log.warn.call_count == 0
  516. def test_no_warning_in_normal_mode(self):
  517. self.mock_client.info.return_value = {'Swarm': {'LocalNodeState': 'inactive'}}
  518. project = Project('composetest', [], self.mock_client)
  519. with mock.patch('compose.project.log') as fake_log:
  520. project.up()
  521. assert fake_log.warn.call_count == 0
  522. def test_no_warning_with_no_swarm_info(self):
  523. self.mock_client.info.return_value = {}
  524. project = Project('composetest', [], self.mock_client)
  525. with mock.patch('compose.project.log') as fake_log:
  526. project.up()
  527. assert fake_log.warn.call_count == 0
  528. def test_no_such_service_unicode(self):
  529. assert NoSuchService('十六夜 咲夜'.encode('utf-8')).msg == 'No such service: 十六夜 咲夜'
  530. assert NoSuchService('十六夜 咲夜').msg == 'No such service: 十六夜 咲夜'
  531. def test_project_platform_value(self):
  532. service_config = {
  533. 'name': 'web',
  534. 'image': 'busybox:latest',
  535. }
  536. config_data = Config(
  537. version=V2_4, services=[service_config], networks={}, volumes={}, secrets=None, configs=None
  538. )
  539. project = Project.from_config(name='test', client=self.mock_client, config_data=config_data)
  540. assert project.get_service('web').platform is None
  541. project = Project.from_config(
  542. name='test', client=self.mock_client, config_data=config_data, default_platform='windows'
  543. )
  544. assert project.get_service('web').platform == 'windows'
  545. service_config['platform'] = 'linux/s390x'
  546. project = Project.from_config(name='test', client=self.mock_client, config_data=config_data)
  547. assert project.get_service('web').platform == 'linux/s390x'
  548. project = Project.from_config(
  549. name='test', client=self.mock_client, config_data=config_data, default_platform='windows'
  550. )
  551. assert project.get_service('web').platform == 'linux/s390x'
  552. @mock.patch('compose.parallel.ParallelStreamWriter._write_noansi')
  553. def test_error_parallel_pull(self, mock_write):
  554. project = Project.from_config(
  555. name='test',
  556. client=self.mock_client,
  557. config_data=Config(
  558. version=V2_0,
  559. services=[{
  560. 'name': 'web',
  561. 'image': 'busybox:latest',
  562. }],
  563. networks=None,
  564. volumes=None,
  565. secrets=None,
  566. configs=None,
  567. ),
  568. )
  569. self.mock_client.pull.side_effect = OperationFailedError('pull error')
  570. with pytest.raises(ProjectError):
  571. project.pull(parallel_pull=True)
  572. self.mock_client.pull.side_effect = OperationFailedError(b'pull error')
  573. with pytest.raises(ProjectError):
  574. project.pull(parallel_pull=True)