1
0

test_docker_container.py 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181
  1. # -*- coding: utf-8 -*-
  2. '''
  3. Integration tests for the docker_container states
  4. '''
  5. # Import Python Libs
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. import errno
  8. import functools
  9. import logging
  10. import os
  11. import subprocess
  12. import tempfile
  13. # Import Salt Testing Libs
  14. from tests.support.unit import skipIf
  15. from tests.support.case import ModuleCase
  16. from tests.support.docker import with_network, random_name
  17. from tests.support.paths import FILES, TMP
  18. from tests.support.helpers import destructiveTest, with_tempdir
  19. from tests.support.mixins import SaltReturnAssertsMixin
  20. # Import Salt Libs
  21. import salt.utils.files
  22. import salt.utils.network
  23. import salt.utils.path
  24. from salt.exceptions import CommandExecutionError
  25. # Import 3rd-party libs
  26. from salt.ext import six
  27. log = logging.getLogger(__name__)
  28. IPV6_ENABLED = bool(salt.utils.network.ip_addrs6(include_loopback=True))
  29. def container_name(func):
  30. '''
  31. Generate a randomized name for a container and clean it up afterward
  32. '''
  33. @functools.wraps(func)
  34. def wrapper(self, *args, **kwargs):
  35. name = random_name(prefix='salt_test_')
  36. try:
  37. return func(self, name, *args, **kwargs)
  38. finally:
  39. try:
  40. self.run_function('docker.rm', [name], force=True)
  41. except CommandExecutionError as exc:
  42. if 'No such container' not in exc.__str__():
  43. raise
  44. return wrapper
  45. @destructiveTest
  46. @skipIf(not salt.utils.path.which('busybox'), 'Busybox not installed')
  47. @skipIf(not salt.utils.path.which('dockerd'), 'Docker not installed')
  48. class DockerContainerTestCase(ModuleCase, SaltReturnAssertsMixin):
  49. '''
  50. Test docker_container states
  51. '''
  52. @classmethod
  53. def setUpClass(cls):
  54. '''
  55. '''
  56. # Create temp dir
  57. cls.image_build_rootdir = tempfile.mkdtemp(dir=TMP)
  58. # Generate image name
  59. cls.image = random_name(prefix='salt_busybox_')
  60. script_path = os.path.join(FILES, 'file/base/mkimage-busybox-static')
  61. cmd = [script_path, cls.image_build_rootdir, cls.image]
  62. log.debug('Running \'%s\' to build busybox image', ' '.join(cmd))
  63. process = subprocess.Popen(
  64. cmd,
  65. close_fds=True,
  66. stdout=subprocess.PIPE,
  67. stderr=subprocess.STDOUT)
  68. output = process.communicate()[0]
  69. log.debug('Output from mkimge-busybox-static:\n%s', output)
  70. if process.returncode != 0:
  71. raise Exception(
  72. 'Failed to build image. Output from mkimge-busybox-static:\n{}'.format(output)
  73. )
  74. try:
  75. salt.utils.files.rm_rf(cls.image_build_rootdir)
  76. except OSError as exc:
  77. if exc.errno != errno.ENOENT:
  78. raise
  79. @classmethod
  80. def tearDownClass(cls):
  81. cmd = ['docker', 'rmi', '--force', cls.image]
  82. log.debug('Running \'%s\' to destroy busybox image', ' '.join(cmd))
  83. process = subprocess.Popen(
  84. cmd,
  85. close_fds=True,
  86. stdout=subprocess.PIPE,
  87. stderr=subprocess.STDOUT)
  88. output = process.communicate()[0]
  89. log.debug('Output from %s:\n%s', ' '.join(cmd), output)
  90. if process.returncode != 0:
  91. raise Exception('Failed to destroy image')
  92. def run_state(self, function, **kwargs):
  93. ret = super(DockerContainerTestCase, self).run_state(function, **kwargs)
  94. log.debug('ret = %s', ret)
  95. return ret
  96. @with_tempdir()
  97. @container_name
  98. def test_running_with_no_predefined_volume(self, name, bind_dir_host):
  99. '''
  100. This tests that a container created using the docker_container.running
  101. state, with binds defined, will also create the corresponding volumes
  102. if they aren't pre-defined in the image.
  103. '''
  104. ret = self.run_state(
  105. 'docker_container.running',
  106. name=name,
  107. image=self.image,
  108. binds=bind_dir_host + ':/foo',
  109. shutdown_timeout=1,
  110. )
  111. self.assertSaltTrueReturn(ret)
  112. # Now check to ensure that the container has volumes to match the
  113. # binds that we used when creating it.
  114. ret = self.run_function('docker.inspect_container', [name])
  115. self.assertTrue('/foo' in ret['Config']['Volumes'])
  116. @container_name
  117. def test_running_with_no_predefined_ports(self, name):
  118. '''
  119. This tests that a container created using the docker_container.running
  120. state, with port_bindings defined, will also configure the
  121. corresponding ports if they aren't pre-defined in the image.
  122. '''
  123. ret = self.run_state(
  124. 'docker_container.running',
  125. name=name,
  126. image=self.image,
  127. port_bindings='14505-14506:24505-24506,2123:2123/udp,8080',
  128. shutdown_timeout=1,
  129. )
  130. self.assertSaltTrueReturn(ret)
  131. # Now check to ensure that the container has ports to match the
  132. # port_bindings that we used when creating it.
  133. expected_ports = (4505, 4506, 8080, '2123/udp')
  134. ret = self.run_function('docker.inspect_container', [name])
  135. self.assertTrue(x in ret['NetworkSettings']['Ports']
  136. for x in expected_ports)
  137. @container_name
  138. def test_running_updated_image_id(self, name):
  139. '''
  140. This tests the case of an image being changed after the container is
  141. created. The next time the state is run, the container should be
  142. replaced because the image ID is now different.
  143. '''
  144. # Create and start a container
  145. ret = self.run_state(
  146. 'docker_container.running',
  147. name=name,
  148. image=self.image,
  149. shutdown_timeout=1,
  150. )
  151. self.assertSaltTrueReturn(ret)
  152. # Get the container's info
  153. c_info = self.run_function('docker.inspect_container', [name])
  154. c_name, c_id = (c_info[x] for x in ('Name', 'Id'))
  155. # Alter the filesystem inside the container
  156. self.assertEqual(
  157. self.run_function('docker.retcode', [name, 'touch /.salttest']),
  158. 0
  159. )
  160. # Commit the changes and overwrite the test class' image
  161. self.run_function('docker.commit', [c_id, self.image])
  162. # Re-run the state
  163. ret = self.run_state(
  164. 'docker_container.running',
  165. name=name,
  166. image=self.image,
  167. shutdown_timeout=1,
  168. )
  169. self.assertSaltTrueReturn(ret)
  170. # Discard the outer dict with the state compiler data to make below
  171. # asserts easier to read/write
  172. ret = ret[next(iter(ret))]
  173. # Check to make sure that the container was replaced
  174. self.assertTrue('container_id' in ret['changes'])
  175. # Check to make sure that the image is in the changes dict, since
  176. # it should have changed
  177. self.assertTrue('image' in ret['changes'])
  178. # Check that the comment in the state return states that
  179. # container's image has changed
  180. self.assertTrue('Container has a new image' in ret['comment'])
  181. @container_name
  182. def test_running_start_false_without_replace(self, name):
  183. '''
  184. Test that we do not start a container which is stopped, when it is not
  185. being replaced.
  186. '''
  187. # Create a container
  188. ret = self.run_state(
  189. 'docker_container.running',
  190. name=name,
  191. image=self.image,
  192. shutdown_timeout=1,
  193. )
  194. self.assertSaltTrueReturn(ret)
  195. # Stop the container
  196. self.run_function('docker.stop', [name], force=True)
  197. # Re-run the state with start=False
  198. ret = self.run_state(
  199. 'docker_container.running',
  200. name=name,
  201. image=self.image,
  202. start=False,
  203. shutdown_timeout=1,
  204. )
  205. self.assertSaltTrueReturn(ret)
  206. # Discard the outer dict with the state compiler data to make below
  207. # asserts easier to read/write
  208. ret = ret[next(iter(ret))]
  209. # Check to make sure that the container was not replaced
  210. self.assertTrue('container_id' not in ret['changes'])
  211. # Check to make sure that the state is not the changes dict, since
  212. # it should not have changed
  213. self.assertTrue('state' not in ret['changes'])
  214. @container_name
  215. def test_running_start_false_with_replace(self, name):
  216. '''
  217. Test that we do start a container which was previously stopped, even
  218. though start=False, because the container was replaced.
  219. '''
  220. # Create a container
  221. ret = self.run_state(
  222. 'docker_container.running',
  223. name=name,
  224. image=self.image,
  225. shutdown_timeout=1,
  226. )
  227. self.assertSaltTrueReturn(ret)
  228. # Stop the container
  229. self.run_function('docker.stop', [name], force=True)
  230. # Re-run the state with start=False but also change the command to
  231. # trigger the container being replaced.
  232. ret = self.run_state(
  233. 'docker_container.running',
  234. name=name,
  235. image=self.image,
  236. command='sleep 600',
  237. start=False,
  238. shutdown_timeout=1,
  239. )
  240. self.assertSaltTrueReturn(ret)
  241. # Discard the outer dict with the state compiler data to make below
  242. # asserts easier to read/write
  243. ret = ret[next(iter(ret))]
  244. # Check to make sure that the container was not replaced
  245. self.assertTrue('container_id' in ret['changes'])
  246. # Check to make sure that the state is not the changes dict, since
  247. # it should not have changed
  248. self.assertTrue('state' not in ret['changes'])
  249. @container_name
  250. def test_running_start_true(self, name):
  251. '''
  252. This tests that we *do* start a container that is stopped, when the
  253. "start" argument is set to True.
  254. '''
  255. # Create a container
  256. ret = self.run_state(
  257. 'docker_container.running',
  258. name=name,
  259. image=self.image,
  260. shutdown_timeout=1,
  261. )
  262. self.assertSaltTrueReturn(ret)
  263. # Stop the container
  264. self.run_function('docker.stop', [name], force=True)
  265. # Re-run the state with start=True
  266. ret = self.run_state(
  267. 'docker_container.running',
  268. name=name,
  269. image=self.image,
  270. start=True,
  271. shutdown_timeout=1,
  272. )
  273. self.assertSaltTrueReturn(ret)
  274. # Discard the outer dict with the state compiler data to make below
  275. # asserts easier to read/write
  276. ret = ret[next(iter(ret))]
  277. # Check to make sure that the container was not replaced
  278. self.assertTrue('container_id' not in ret['changes'])
  279. # Check to make sure that the state is in the changes dict, since
  280. # it should have changed
  281. self.assertTrue('state' in ret['changes'])
  282. # Check that the comment in the state return states that
  283. # container's state has changed
  284. self.assertTrue(
  285. "State changed from 'stopped' to 'running'" in ret['comment'])
  286. @container_name
  287. def test_running_with_invalid_input(self, name):
  288. '''
  289. This tests that the input tranlation code identifies invalid input and
  290. includes information about that invalid argument in the state return.
  291. '''
  292. # Try to create a container with invalid input
  293. ret = self.run_state(
  294. 'docker_container.running',
  295. name=name,
  296. image=self.image,
  297. ulimits='nofile:2048',
  298. shutdown_timeout=1,
  299. )
  300. self.assertSaltFalseReturn(ret)
  301. # Discard the outer dict with the state compiler data to make below
  302. # asserts easier to read/write
  303. ret = ret[next(iter(ret))]
  304. # Check to make sure that the container was not created
  305. self.assertTrue('container_id' not in ret['changes'])
  306. # Check that the error message about the invalid argument is
  307. # included in the comment for the state
  308. self.assertTrue(
  309. 'Ulimit definition \'nofile:2048\' is not in the format '
  310. 'type=soft_limit[:hard_limit]' in ret['comment']
  311. )
  312. @container_name
  313. def test_running_with_argument_collision(self, name):
  314. '''
  315. this tests that the input tranlation code identifies an argument
  316. collision (API args and their aliases being simultaneously used) and
  317. includes information about them in the state return.
  318. '''
  319. # try to create a container with invalid input
  320. ret = self.run_state(
  321. 'docker_container.running',
  322. name=name,
  323. image=self.image,
  324. ulimits='nofile=2048',
  325. ulimit='nofile=1024:2048',
  326. shutdown_timeout=1,
  327. )
  328. self.assertSaltFalseReturn(ret)
  329. # Ciscard the outer dict with the state compiler data to make below
  330. # asserts easier to read/write
  331. ret = ret[next(iter(ret))]
  332. # Check to make sure that the container was not created
  333. self.assertTrue('container_id' not in ret['changes'])
  334. # Check that the error message about the collision is included in
  335. # the comment for the state
  336. self.assertTrue(
  337. '\'ulimit\' is an alias for \'ulimits\'' in ret['comment'])
  338. @container_name
  339. def test_running_with_ignore_collisions(self, name):
  340. '''
  341. This tests that the input tranlation code identifies an argument
  342. collision (API args and their aliases being simultaneously used)
  343. includes information about them in the state return.
  344. '''
  345. # try to create a container with invalid input
  346. ret = self.run_state(
  347. 'docker_container.running',
  348. name=name,
  349. image=self.image,
  350. ignore_collisions=True,
  351. ulimits='nofile=2048',
  352. ulimit='nofile=1024:2048',
  353. shutdown_timeout=1,
  354. )
  355. self.assertSaltTrueReturn(ret)
  356. # Discard the outer dict with the state compiler data to make below
  357. # asserts easier to read/write
  358. ret = ret[next(iter(ret))]
  359. # Check to make sure that the container was created
  360. self.assertTrue('container_id' in ret['changes'])
  361. # Check that the value from the API argument was one that was used
  362. # to create the container
  363. c_info = self.run_function('docker.inspect_container', [name])
  364. actual = c_info['HostConfig']['Ulimits']
  365. expected = [{'Name': 'nofile', 'Soft': 2048, 'Hard': 2048}]
  366. self.assertEqual(actual, expected)
  367. @container_name
  368. def test_running_with_removed_argument(self, name):
  369. '''
  370. This tests that removing an argument from a created container will
  371. be detected and result in the container being replaced.
  372. It also tests that we revert back to the value from the image. This
  373. way, when the "command" argument is removed, we confirm that we are
  374. reverting back to the image's command.
  375. '''
  376. # Create the container
  377. ret = self.run_state(
  378. 'docker_container.running',
  379. name=name,
  380. image=self.image,
  381. command='sleep 600',
  382. shutdown_timeout=1,
  383. )
  384. self.assertSaltTrueReturn(ret)
  385. # Run the state again with the "command" argument removed
  386. ret = self.run_state(
  387. 'docker_container.running',
  388. name=name,
  389. image=self.image,
  390. shutdown_timeout=1,
  391. )
  392. self.assertSaltTrueReturn(ret)
  393. # Discard the outer dict with the state compiler data to make below
  394. # asserts easier to read/write
  395. ret = ret[next(iter(ret))]
  396. # Now check to ensure that the changes include the command
  397. # reverting back to the image's command.
  398. image_info = self.run_function('docker.inspect_image', [self.image])
  399. self.assertEqual(
  400. ret['changes']['container']['Config']['Cmd']['new'],
  401. image_info['Config']['Cmd']
  402. )
  403. @container_name
  404. def test_running_with_port_bindings(self, name):
  405. '''
  406. This tests that the ports which are being bound are also exposed, even
  407. when not explicitly configured. This test will create a container with
  408. only some of the ports exposed, including some which aren't even bound.
  409. The resulting containers exposed ports should contain all of the ports
  410. defined in the "ports" argument, as well as each of the ports which are
  411. being bound.
  412. '''
  413. # Create the container
  414. ret = self.run_state(
  415. 'docker_container.running',
  416. name=name,
  417. image=self.image,
  418. command='sleep 600',
  419. shutdown_timeout=1,
  420. port_bindings=[1234, '1235-1236', '2234/udp', '2235-2236/udp'],
  421. ports=[1235, '2235/udp', 9999],
  422. )
  423. self.assertSaltTrueReturn(ret)
  424. # Check the created container's port bindings and exposed ports. The
  425. # port bindings should only contain the ports defined in the
  426. # port_bindings argument, while the exposed ports should also contain
  427. # the extra port (9999/tcp) which was included in the ports argument.
  428. cinfo = self.run_function('docker.inspect_container', [name])
  429. ports = ['1234/tcp', '1235/tcp', '1236/tcp',
  430. '2234/udp', '2235/udp', '2236/udp']
  431. self.assertEqual(
  432. sorted(cinfo['HostConfig']['PortBindings']),
  433. ports
  434. )
  435. self.assertEqual(
  436. sorted(cinfo['Config']['ExposedPorts']),
  437. ports + ['9999/tcp']
  438. )
  439. @container_name
  440. def test_absent_with_stopped_container(self, name):
  441. '''
  442. This tests the docker_container.absent state on a stopped container
  443. '''
  444. # Create the container
  445. self.run_function('docker.create', [self.image], name=name)
  446. # Remove the container
  447. ret = self.run_state(
  448. 'docker_container.absent',
  449. name=name,
  450. )
  451. self.assertSaltTrueReturn(ret)
  452. # Discard the outer dict with the state compiler data to make below
  453. # asserts easier to read/write
  454. ret = ret[next(iter(ret))]
  455. # Check that we have a removed container ID in the changes dict
  456. self.assertTrue('removed' in ret['changes'])
  457. # Run the state again to confirm it changes nothing
  458. ret = self.run_state(
  459. 'docker_container.absent',
  460. name=name,
  461. )
  462. self.assertSaltTrueReturn(ret)
  463. # Discard the outer dict with the state compiler data to make below
  464. # asserts easier to read/write
  465. ret = ret[next(iter(ret))]
  466. # Nothing should have changed
  467. self.assertEqual(ret['changes'], {})
  468. # Ensure that the comment field says the container does not exist
  469. self.assertEqual(
  470. ret['comment'],
  471. 'Container \'{0}\' does not exist'.format(name)
  472. )
  473. @container_name
  474. def test_absent_with_running_container(self, name):
  475. '''
  476. This tests the docker_container.absent state and
  477. '''
  478. # Create the container
  479. ret = self.run_state(
  480. 'docker_container.running',
  481. name=name,
  482. image=self.image,
  483. command='sleep 600',
  484. shutdown_timeout=1,
  485. )
  486. self.assertSaltTrueReturn(ret)
  487. # Try to remove the container. This should fail because force=True
  488. # is needed to remove a container that is running.
  489. ret = self.run_state(
  490. 'docker_container.absent',
  491. name=name,
  492. )
  493. self.assertSaltFalseReturn(ret)
  494. # Discard the outer dict with the state compiler data to make below
  495. # asserts easier to read/write
  496. ret = ret[next(iter(ret))]
  497. # Nothing should have changed
  498. self.assertEqual(ret['changes'], {})
  499. # Ensure that the comment states that force=True is required
  500. self.assertEqual(
  501. ret['comment'],
  502. 'Container is running, set force to True to forcibly remove it'
  503. )
  504. # Try again with force=True. This should succeed.
  505. ret = self.run_state('docker_container.absent',
  506. name=name,
  507. force=True,
  508. )
  509. self.assertSaltTrueReturn(ret)
  510. # Discard the outer dict with the state compiler data to make below
  511. # asserts easier to read/write
  512. ret = ret[next(iter(ret))]
  513. # Check that we have a removed container ID in the changes dict
  514. self.assertTrue('removed' in ret['changes'])
  515. # The comment should mention that the container was removed
  516. self.assertEqual(
  517. ret['comment'],
  518. 'Forcibly removed container \'{0}\''.format(name)
  519. )
  520. @container_name
  521. def test_running_image_name(self, name):
  522. '''
  523. Ensure that we create the container using the image name instead of ID
  524. '''
  525. ret = self.run_state(
  526. 'docker_container.running',
  527. name=name,
  528. image=self.image,
  529. shutdown_timeout=1,
  530. )
  531. self.assertSaltTrueReturn(ret)
  532. ret = self.run_function('docker.inspect_container', [name])
  533. self.assertEqual(ret['Config']['Image'], self.image)
  534. @container_name
  535. def test_env_with_running_container(self, name):
  536. '''
  537. docker_container.running environnment part. Testing issue 39838.
  538. '''
  539. ret = self.run_state(
  540. 'docker_container.running',
  541. name=name,
  542. image=self.image,
  543. env='VAR1=value1,VAR2=value2,VAR3=value3',
  544. shutdown_timeout=1,
  545. )
  546. self.assertSaltTrueReturn(ret)
  547. ret = self.run_function('docker.inspect_container', [name])
  548. self.assertTrue('VAR1=value1' in ret['Config']['Env'])
  549. self.assertTrue('VAR2=value2' in ret['Config']['Env'])
  550. self.assertTrue('VAR3=value3' in ret['Config']['Env'])
  551. ret = self.run_state(
  552. 'docker_container.running',
  553. name=name,
  554. image=self.image,
  555. env='VAR1=value1,VAR2=value2',
  556. shutdown_timeout=1,
  557. )
  558. self.assertSaltTrueReturn(ret)
  559. ret = self.run_function('docker.inspect_container', [name])
  560. self.assertTrue('VAR1=value1' in ret['Config']['Env'])
  561. self.assertTrue('VAR2=value2' in ret['Config']['Env'])
  562. self.assertTrue('VAR3=value3' not in ret['Config']['Env'])
  563. @with_network(subnet='10.247.197.96/27', create=True)
  564. @container_name
  565. def test_static_ip_one_network(self, container_name, net):
  566. '''
  567. Ensure that if a network is created and specified as network_mode, that is the only network, and
  568. the static IP is applied.
  569. '''
  570. requested_ip = '10.247.197.100'
  571. kwargs = {
  572. 'name': container_name,
  573. 'image': self.image,
  574. 'network_mode': net.name,
  575. 'networks': [{net.name: [{'ipv4_address': requested_ip}]}],
  576. 'shutdown_timeout': 1,
  577. }
  578. # Create a container
  579. ret = self.run_state('docker_container.running', **kwargs)
  580. self.assertSaltTrueReturn(ret)
  581. inspect_result = self.run_function('docker.inspect_container',
  582. [container_name])
  583. connected_networks = inspect_result['NetworkSettings']['Networks']
  584. self.assertEqual(list(connected_networks.keys()), [net.name])
  585. self.assertEqual(inspect_result['HostConfig']['NetworkMode'], net.name)
  586. self.assertEqual(connected_networks[net.name]['IPAMConfig']['IPv4Address'], requested_ip)
  587. def _test_running(self, container_name, *nets):
  588. '''
  589. DRY function for testing static IPs
  590. '''
  591. networks = []
  592. for net in nets:
  593. net_def = {
  594. net.name: [
  595. {net.ip_arg: net[0]}
  596. ]
  597. }
  598. networks.append(net_def)
  599. kwargs = {
  600. 'name': container_name,
  601. 'image': self.image,
  602. 'networks': networks,
  603. 'shutdown_timeout': 1,
  604. }
  605. # Create a container
  606. ret = self.run_state('docker_container.running', **kwargs)
  607. self.assertSaltTrueReturn(ret)
  608. inspect_result = self.run_function('docker.inspect_container',
  609. [container_name])
  610. connected_networks = inspect_result['NetworkSettings']['Networks']
  611. # Check that the correct IP was set
  612. try:
  613. for net in nets:
  614. self.assertEqual(
  615. connected_networks[net.name]['IPAMConfig'][net.arg_map(net.ip_arg)],
  616. net[0]
  617. )
  618. except KeyError:
  619. # Fail with a meaningful error
  620. msg = (
  621. 'Container does not have the expected network config for '
  622. 'network {0}'.format(net.name)
  623. )
  624. log.error(msg)
  625. log.error('Connected networks: %s', connected_networks)
  626. self.fail('{0}. See log for more information.'.format(msg))
  627. # Check that container continued running and didn't immediately exit
  628. self.assertTrue(inspect_result['State']['Running'])
  629. # Update the SLS configuration to use the second random IP so that we
  630. # can test updating a container's network configuration without
  631. # replacing the container.
  632. for idx, net in enumerate(nets):
  633. kwargs['networks'][idx][net.name][0][net.ip_arg] = net[1]
  634. ret = self.run_state('docker_container.running', **kwargs)
  635. self.assertSaltTrueReturn(ret)
  636. ret = ret[next(iter(ret))]
  637. expected = {'container': {'Networks': {}}}
  638. for net in nets:
  639. expected['container']['Networks'][net.name] = {
  640. 'IPAMConfig': {
  641. 'old': {net.arg_map(net.ip_arg): net[0]},
  642. 'new': {net.arg_map(net.ip_arg): net[1]},
  643. }
  644. }
  645. self.assertEqual(ret['changes'], expected)
  646. expected = [
  647. "Container '{0}' is already configured as specified.".format(
  648. container_name
  649. )
  650. ]
  651. expected.extend([
  652. "Reconnected to network '{0}' with updated configuration.".format(
  653. x.name
  654. )
  655. for x in sorted(nets, key=lambda y: y.name)
  656. ])
  657. expected = ' '.join(expected)
  658. self.assertEqual(ret['comment'], expected)
  659. # Update the SLS configuration to remove the last network
  660. kwargs['networks'].pop(-1)
  661. ret = self.run_state('docker_container.running', **kwargs)
  662. self.assertSaltTrueReturn(ret)
  663. ret = ret[next(iter(ret))]
  664. expected = {
  665. 'container': {
  666. 'Networks': {
  667. nets[-1].name: {
  668. 'IPAMConfig': {
  669. 'old': {
  670. nets[-1].arg_map(nets[-1].ip_arg): nets[-1][1]
  671. },
  672. 'new': None,
  673. }
  674. }
  675. }
  676. }
  677. }
  678. self.assertEqual(ret['changes'], expected)
  679. expected = (
  680. "Container '{0}' is already configured as specified. Disconnected "
  681. "from network '{1}'.".format(container_name, nets[-1].name)
  682. )
  683. self.assertEqual(ret['comment'], expected)
  684. # Update the SLS configuration to add back the last network, only use
  685. # an automatic IP instead of static IP.
  686. kwargs['networks'].append(nets[-1].name)
  687. ret = self.run_state('docker_container.running', **kwargs)
  688. self.assertSaltTrueReturn(ret)
  689. ret = ret[next(iter(ret))]
  690. # Get the automatic IP by inspecting the container, and use it to build
  691. # the expected changes.
  692. container_netinfo = self.run_function(
  693. 'docker.inspect_container',
  694. [container_name]).get('NetworkSettings', {}).get('Networks', {})[nets[-1].name]
  695. autoip_keys = self.minion_opts['docker.compare_container_networks']['automatic']
  696. autoip_config = {
  697. x: y for x, y in six.iteritems(container_netinfo)
  698. if x in autoip_keys and y
  699. }
  700. expected = {'container': {'Networks': {nets[-1].name: {}}}}
  701. for key, val in six.iteritems(autoip_config):
  702. expected['container']['Networks'][nets[-1].name][key] = {
  703. 'old': None, 'new': val
  704. }
  705. self.assertEqual(ret['changes'], expected)
  706. expected = (
  707. "Container '{0}' is already configured as specified. Connected "
  708. "to network '{1}'.".format(container_name, nets[-1].name)
  709. )
  710. self.assertEqual(ret['comment'], expected)
  711. # Update the SLS configuration to remove the last network
  712. kwargs['networks'].pop(-1)
  713. ret = self.run_state('docker_container.running', **kwargs)
  714. self.assertSaltTrueReturn(ret)
  715. ret = ret[next(iter(ret))]
  716. expected = {'container': {'Networks': {nets[-1].name: {}}}}
  717. for key, val in six.iteritems(autoip_config):
  718. expected['container']['Networks'][nets[-1].name][key] = {
  719. 'old': val, 'new': None
  720. }
  721. self.assertEqual(ret['changes'], expected)
  722. expected = (
  723. "Container '{0}' is already configured as specified. Disconnected "
  724. "from network '{1}'.".format(container_name, nets[-1].name)
  725. )
  726. self.assertEqual(ret['comment'], expected)
  727. @with_network(subnet='10.247.197.96/27', create=True)
  728. @container_name
  729. def test_running_ipv4(self, container_name, *nets):
  730. self._test_running(container_name, *nets)
  731. @with_network(subnet='10.247.197.128/27', create=True)
  732. @with_network(subnet='10.247.197.96/27', create=True)
  733. @container_name
  734. def test_running_dual_ipv4(self, container_name, *nets):
  735. self._test_running(container_name, *nets)
  736. @with_network(subnet='fe3f:2180:26:1::/123', create=True)
  737. @container_name
  738. @skipIf(not IPV6_ENABLED, 'IPv6 not enabled')
  739. def test_running_ipv6(self, container_name, *nets):
  740. self._test_running(container_name, *nets)
  741. @with_network(subnet='fe3f:2180:26:1::20/123', create=True)
  742. @with_network(subnet='fe3f:2180:26:1::/123', create=True)
  743. @container_name
  744. @skipIf(not IPV6_ENABLED, 'IPv6 not enabled')
  745. def test_running_dual_ipv6(self, container_name, *nets):
  746. self._test_running(container_name, *nets)
  747. @with_network(subnet='fe3f:2180:26:1::/123', create=True)
  748. @with_network(subnet='10.247.197.96/27', create=True)
  749. @container_name
  750. @skipIf(not IPV6_ENABLED, 'IPv6 not enabled')
  751. def test_running_mixed_ipv4_and_ipv6(self, container_name, *nets):
  752. self._test_running(container_name, *nets)
  753. @with_network(subnet='10.247.197.96/27', create=True)
  754. @container_name
  755. def test_running_explicit_networks(self, container_name, net):
  756. '''
  757. Ensure that if we use an explicit network configuration, we remove any
  758. default networks not specified (e.g. the default "bridge" network).
  759. '''
  760. # Create a container with no specific network configuration. The only
  761. # networks connected will be the default ones.
  762. ret = self.run_state(
  763. 'docker_container.running',
  764. name=container_name,
  765. image=self.image,
  766. shutdown_timeout=1)
  767. self.assertSaltTrueReturn(ret)
  768. inspect_result = self.run_function('docker.inspect_container',
  769. [container_name])
  770. # Get the default network names
  771. default_networks = list(inspect_result['NetworkSettings']['Networks'])
  772. # Re-run the state with an explicit network configuration. All of the
  773. # default networks should be disconnected.
  774. ret = self.run_state(
  775. 'docker_container.running',
  776. name=container_name,
  777. image=self.image,
  778. networks=[net.name],
  779. shutdown_timeout=1)
  780. self.assertSaltTrueReturn(ret)
  781. ret = ret[next(iter(ret))]
  782. net_changes = ret['changes']['container']['Networks']
  783. self.assertIn(
  784. "Container '{0}' is already configured as specified.".format(
  785. container_name
  786. ),
  787. ret['comment']
  788. )
  789. updated_networks = self.run_function(
  790. 'docker.inspect_container',
  791. [container_name])['NetworkSettings']['Networks']
  792. for default_network in default_networks:
  793. self.assertIn(
  794. "Disconnected from network '{0}'.".format(default_network),
  795. ret['comment']
  796. )
  797. self.assertIn(default_network, net_changes)
  798. # We've tested that the state return is correct, but let's be extra
  799. # paranoid and check the actual connected networks.
  800. self.assertNotIn(default_network, updated_networks)
  801. self.assertIn(
  802. "Connected to network '{0}'.".format(net.name),
  803. ret['comment']
  804. )
  805. @container_name
  806. def test_run_with_onlyif(self, name):
  807. '''
  808. Test docker_container.run with onlyif. The container should not run
  809. (and the state should return a True result) if the onlyif has a nonzero
  810. return code, but if the onlyif has a zero return code the container
  811. should run.
  812. '''
  813. for cmd in ('/bin/false', ['/bin/true', '/bin/false']):
  814. log.debug('Trying %s', cmd)
  815. ret = self.run_state(
  816. 'docker_container.run',
  817. name=name,
  818. image=self.image,
  819. command='whoami',
  820. onlyif=cmd)
  821. self.assertSaltTrueReturn(ret)
  822. ret = ret[next(iter(ret))]
  823. self.assertFalse(ret['changes'])
  824. self.assertTrue(
  825. ret['comment'].startswith(
  826. 'onlyif command /bin/false returned exit code of'
  827. )
  828. )
  829. self.run_function('docker.rm', [name], force=True)
  830. for cmd in ('/bin/true', ['/bin/true', 'ls /']):
  831. log.debug('Trying %s', cmd)
  832. ret = self.run_state(
  833. 'docker_container.run',
  834. name=name,
  835. image=self.image,
  836. command='whoami',
  837. onlyif=cmd)
  838. self.assertSaltTrueReturn(ret)
  839. ret = ret[next(iter(ret))]
  840. self.assertEqual(ret['changes']['Logs'], 'root\n')
  841. self.assertEqual(
  842. ret['comment'],
  843. 'Container ran and exited with a return code of 0'
  844. )
  845. self.run_function('docker.rm', [name], force=True)
  846. @container_name
  847. def test_run_with_unless(self, name):
  848. '''
  849. Test docker_container.run with unless. The container should not run
  850. (and the state should return a True result) if the unless has a zero
  851. return code, but if the unless has a nonzero return code the container
  852. should run.
  853. '''
  854. for cmd in ('/bin/true', ['/bin/false', '/bin/true']):
  855. log.debug('Trying %s', cmd)
  856. ret = self.run_state(
  857. 'docker_container.run',
  858. name=name,
  859. image=self.image,
  860. command='whoami',
  861. unless=cmd)
  862. self.assertSaltTrueReturn(ret)
  863. ret = ret[next(iter(ret))]
  864. self.assertFalse(ret['changes'])
  865. self.assertEqual(
  866. ret['comment'],
  867. 'unless command /bin/true returned exit code of 0'
  868. )
  869. self.run_function('docker.rm', [name], force=True)
  870. for cmd in ('/bin/false', ['/bin/false', 'ls /paththatdoesnotexist']):
  871. log.debug('Trying %s', cmd)
  872. ret = self.run_state(
  873. 'docker_container.run',
  874. name=name,
  875. image=self.image,
  876. command='whoami',
  877. unless=cmd)
  878. self.assertSaltTrueReturn(ret)
  879. ret = ret[next(iter(ret))]
  880. self.assertEqual(ret['changes']['Logs'], 'root\n')
  881. self.assertEqual(
  882. ret['comment'],
  883. 'Container ran and exited with a return code of 0'
  884. )
  885. self.run_function('docker.rm', [name], force=True)
  886. @container_name
  887. def test_run_with_creates(self, name):
  888. '''
  889. Test docker_container.run with creates. The container should not run
  890. (and the state should return a True result) if all of the files exist,
  891. but if if any of the files do not exist the container should run.
  892. '''
  893. def _mkstemp():
  894. fd, ret = tempfile.mkstemp()
  895. try:
  896. os.close(fd)
  897. except OSError as exc:
  898. if exc.errno != errno.EBADF:
  899. raise exc
  900. else:
  901. self.addCleanup(os.remove, ret)
  902. return ret
  903. bad_file = '/tmp/filethatdoesnotexist'
  904. good_file1 = _mkstemp()
  905. good_file2 = _mkstemp()
  906. for path in (good_file1, [good_file1, good_file2]):
  907. log.debug('Trying %s', path)
  908. ret = self.run_state(
  909. 'docker_container.run',
  910. name=name,
  911. image=self.image,
  912. command='whoami',
  913. creates=path)
  914. self.assertSaltTrueReturn(ret)
  915. ret = ret[next(iter(ret))]
  916. self.assertFalse(ret['changes'])
  917. self.assertEqual(
  918. ret['comment'],
  919. 'All specified paths in \'creates\' argument exist'
  920. )
  921. self.run_function('docker.rm', [name], force=True)
  922. for path in (bad_file, [good_file1, bad_file]):
  923. log.debug('Trying %s', path)
  924. ret = self.run_state(
  925. 'docker_container.run',
  926. name=name,
  927. image=self.image,
  928. command='whoami',
  929. creates=path)
  930. self.assertSaltTrueReturn(ret)
  931. ret = ret[next(iter(ret))]
  932. self.assertEqual(ret['changes']['Logs'], 'root\n')
  933. self.assertEqual(
  934. ret['comment'],
  935. 'Container ran and exited with a return code of 0'
  936. )
  937. self.run_function('docker.rm', [name], force=True)
  938. @container_name
  939. def test_run_replace(self, name):
  940. '''
  941. Test the replace and force arguments to make sure they work properly
  942. '''
  943. # Run once to create the container
  944. ret = self.run_state(
  945. 'docker_container.run',
  946. name=name,
  947. image=self.image,
  948. command='whoami')
  949. self.assertSaltTrueReturn(ret)
  950. ret = ret[next(iter(ret))]
  951. self.assertEqual(ret['changes']['Logs'], 'root\n')
  952. self.assertEqual(
  953. ret['comment'],
  954. 'Container ran and exited with a return code of 0'
  955. )
  956. # Run again with replace=False, this should fail
  957. ret = self.run_state(
  958. 'docker_container.run',
  959. name=name,
  960. image=self.image,
  961. command='whoami',
  962. replace=False)
  963. self.assertSaltFalseReturn(ret)
  964. ret = ret[next(iter(ret))]
  965. self.assertFalse(ret['changes'])
  966. self.assertEqual(
  967. ret['comment'],
  968. 'Encountered error running container: Container \'{0}\' exists. '
  969. 'Run with replace=True to remove the existing container'.format(name)
  970. )
  971. # Run again with replace=True, this should proceed and there should be
  972. # a "Replaces" key in the changes dict to show that a container was
  973. # replaced.
  974. ret = self.run_state(
  975. 'docker_container.run',
  976. name=name,
  977. image=self.image,
  978. command='whoami',
  979. replace=True)
  980. self.assertSaltTrueReturn(ret)
  981. ret = ret[next(iter(ret))]
  982. self.assertEqual(ret['changes']['Logs'], 'root\n')
  983. self.assertTrue('Replaces' in ret['changes'])
  984. self.assertEqual(
  985. ret['comment'],
  986. 'Container ran and exited with a return code of 0'
  987. )
  988. @container_name
  989. def test_run_force(self, name):
  990. '''
  991. Test the replace and force arguments to make sure they work properly
  992. '''
  993. # Start up a container that will stay running
  994. ret = self.run_state(
  995. 'docker_container.running',
  996. name=name,
  997. image=self.image)
  998. self.assertSaltTrueReturn(ret)
  999. # Run again with replace=True, this should fail because the container
  1000. # is still running
  1001. ret = self.run_state(
  1002. 'docker_container.run',
  1003. name=name,
  1004. image=self.image,
  1005. command='whoami',
  1006. replace=True,
  1007. force=False)
  1008. self.assertSaltFalseReturn(ret)
  1009. ret = ret[next(iter(ret))]
  1010. self.assertFalse(ret['changes'])
  1011. self.assertEqual(
  1012. ret['comment'],
  1013. 'Encountered error running container: Container \'{0}\' exists '
  1014. 'and is running. Run with replace=True and force=True to force '
  1015. 'removal of the existing container.'.format(name)
  1016. )
  1017. # Run again with replace=True and force=True, this should proceed and
  1018. # there should be a "Replaces" key in the changes dict to show that a
  1019. # container was replaced.
  1020. ret = self.run_state(
  1021. 'docker_container.run',
  1022. name=name,
  1023. image=self.image,
  1024. command='whoami',
  1025. replace=True,
  1026. force=True)
  1027. self.assertSaltTrueReturn(ret)
  1028. ret = ret[next(iter(ret))]
  1029. self.assertEqual(ret['changes']['Logs'], 'root\n')
  1030. self.assertTrue('Replaces' in ret['changes'])
  1031. self.assertEqual(
  1032. ret['comment'],
  1033. 'Container ran and exited with a return code of 0'
  1034. )
  1035. @container_name
  1036. def test_run_failhard(self, name):
  1037. '''
  1038. Test to make sure that we fail a state when the container exits with
  1039. nonzero status if failhard is set to True, and that we don't when it is
  1040. set to False.
  1041. '''
  1042. ret = self.run_state(
  1043. 'docker_container.run',
  1044. name=name,
  1045. image=self.image,
  1046. command='/bin/false',
  1047. failhard=True)
  1048. self.assertSaltFalseReturn(ret)
  1049. ret = ret[next(iter(ret))]
  1050. self.assertEqual(ret['changes']['Logs'], '')
  1051. self.assertTrue(
  1052. ret['comment'].startswith(
  1053. 'Container ran and exited with a return code of'
  1054. )
  1055. )
  1056. self.run_function('docker.rm', [name], force=True)
  1057. ret = self.run_state(
  1058. 'docker_container.run',
  1059. name=name,
  1060. image=self.image,
  1061. command='/bin/false',
  1062. failhard=False)
  1063. self.assertSaltTrueReturn(ret)
  1064. ret = ret[next(iter(ret))]
  1065. self.assertEqual(ret['changes']['Logs'], '')
  1066. self.assertTrue(
  1067. ret['comment'].startswith(
  1068. 'Container ran and exited with a return code of'
  1069. )
  1070. )
  1071. self.run_function('docker.rm', [name], force=True)
  1072. @container_name
  1073. def test_run_bg(self, name):
  1074. '''
  1075. Test to make sure that if the container is run in the background, we do
  1076. not include an ExitCode or Logs key in the return. Then check the logs
  1077. for the container to ensure that it ran as expected.
  1078. '''
  1079. ret = self.run_state(
  1080. 'docker_container.run',
  1081. name=name,
  1082. image=self.image,
  1083. command='sh -c "sleep 5 && whoami"',
  1084. bg=True)
  1085. self.assertSaltTrueReturn(ret)
  1086. ret = ret[next(iter(ret))]
  1087. self.assertTrue('Logs' not in ret['changes'])
  1088. self.assertTrue('ExitCode' not in ret['changes'])
  1089. self.assertEqual(ret['comment'], 'Container was run in the background')
  1090. # Now check the logs. The expectation is that the above asserts
  1091. # completed during the 5-second sleep.
  1092. self.assertEqual(
  1093. self.run_function('docker.logs', [name], follow=True),
  1094. 'root\n'
  1095. )