1
0

test_docker_container.py 46 KB

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