test_docker_container.py 45 KB

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