github.com/containers/podman/v5@v5.1.0-rc1/test/python/docker/compat/test_containers.py (about) 1 """ 2 Integration tests for exercising docker-py against Podman Service. 3 """ 4 import io 5 import json 6 import tarfile 7 import threading 8 import time 9 from typing import IO, List, Optional 10 11 import yaml 12 from docker import errors 13 from docker.models.containers import Container 14 from docker.models.images import Image 15 from docker.models.volumes import Volume 16 from docker.types import Mount 17 from jsonschema.exceptions import best_match, ValidationError 18 19 # pylint: disable=no-name-in-module,import-error,wrong-import-order 20 from test.python.docker.compat import common, constant 21 from openapi_schema_validator import OAS31Validator 22 23 from test.python.docker.compat.constant import DOCKER_API_COMPATIBILITY_VERSION 24 25 26 # pylint: disable=missing-function-docstring 27 class TestContainers(common.DockerTestCase): 28 """TestCase for exercising containers.""" 29 30 def test_create_container(self): 31 """Run a container with detach mode.""" 32 self.docker.containers.create(image="alpine", detach=True) 33 self.assertEqual(len(self.docker.containers.list(all=True)), 2) 34 35 def test_create_network(self): 36 """Add network to a container.""" 37 self.docker.networks.create("testNetwork", driver="bridge") 38 self.docker.containers.create(image="alpine", detach=True) 39 40 def test_start_container(self): 41 # Podman docs says it should give a 304 but returns with no response 42 # # Start an already started container should return 304 43 # response = self.docker.api.start(container=self.top_container_id) 44 # self.assertEqual(error.exception.response.status_code, 304) 45 46 # Create a new container and validate the count 47 self.docker.containers.create(image=constant.ALPINE, name="container2") 48 containers = self.docker.containers.list(all=True) 49 self.assertEqual(len(containers), 2) 50 51 def test_start_container_with_random_port_bind(self): 52 container = self.docker.containers.create( 53 image=constant.ALPINE, 54 name="containerWithRandomBind", 55 ports={"1234/tcp": None}, 56 ) 57 containers = self.docker.containers.list(all=True) 58 self.assertTrue(container in containers) 59 60 def test_stop_container(self): 61 top = self.docker.containers.get(self.top_container_id) 62 self.assertEqual(top.status, "running") 63 64 # Stop a running container and validate the state 65 top.stop() 66 top.reload() 67 self.assertIn(top.status, ("stopped", "exited")) 68 69 def test_kill_container(self): 70 top = self.docker.containers.get(self.top_container_id) 71 self.assertEqual(top.status, "running") 72 73 # Kill a running container and validate the state 74 top.kill() 75 top.reload() 76 self.assertIn(top.status, ("stopped", "exited")) 77 78 def test_restart_container(self): 79 # Validate the container state 80 top = self.docker.containers.get(self.top_container_id) 81 top.stop() 82 top.reload() 83 self.assertIn(top.status, ("stopped", "exited")) 84 85 # restart a running container and validate the state 86 top.restart() 87 top.reload() 88 self.assertEqual(top.status, "running") 89 90 def test_remove_container(self): 91 # Remove container by ID with force 92 top = self.docker.containers.get(self.top_container_id) 93 top.remove(force=True) 94 self.assertEqual(len(self.docker.containers.list()), 0) 95 96 def test_remove_container_without_force(self): 97 # Validate current container count 98 self.assertEqual(len(self.docker.containers.list()), 1) 99 100 # Remove running container should throw error 101 top = self.docker.containers.get(self.top_container_id) 102 with self.assertRaises(errors.APIError) as error: 103 top.remove() 104 self.assertEqual(error.exception.response.status_code, 500) 105 106 # Remove container by ID without force 107 top.stop() 108 top.remove() 109 self.assertEqual(len(self.docker.containers.list()), 0) 110 111 def test_pause_container(self): 112 # Validate the container state 113 top = self.docker.containers.get(self.top_container_id) 114 self.assertEqual(top.status, "running") 115 116 # Pause a running container and validate the state 117 top.pause() 118 top.reload() 119 self.assertEqual(top.status, "paused") 120 121 def test_pause_stopped_container(self): 122 # Stop the container 123 top = self.docker.containers.get(self.top_container_id) 124 top.stop() 125 126 # Pause exited container should throw error 127 with self.assertRaises(errors.APIError) as error: 128 top.pause() 129 self.assertEqual(error.exception.response.status_code, 500) 130 131 def test_unpause_container(self): 132 top = self.docker.containers.get(self.top_container_id) 133 134 # Validate the container state 135 top.pause() 136 top.reload() 137 self.assertEqual(top.status, "paused") 138 139 # Pause a running container and validate the state 140 top.unpause() 141 top.reload() 142 self.assertEqual(top.status, "running") 143 144 def test_list_container(self): 145 # Add container and validate the count 146 self.docker.containers.create(image="alpine", detach=True) 147 containers = self.docker.containers.list(all=True) 148 self.assertEqual(len(containers), 2) 149 150 def test_filters(self): 151 self.skipTest("TODO Endpoint does not yet support filters") 152 153 # List container with filter by id 154 filters = {"id": self.top_container_id} 155 ctnrs = self.docker.containers.list(all=True, filters=filters) 156 self.assertEqual(len(ctnrs), 1) 157 158 # List container with filter by name 159 filters = {"name": "top"} 160 ctnrs = self.docker.containers.list(all=True, filters=filters) 161 self.assertEqual(len(ctnrs), 1) 162 163 def test_copy_to_container(self): 164 ctr: Optional[Container] = None 165 vol: Optional[Volume] = None 166 try: 167 test_file_content = b"Hello World!" 168 vol = self.docker.volumes.create("test-volume") 169 ctr = self.docker.containers.create( 170 image="alpine", 171 detach=True, 172 command="top", 173 volumes=["test-volume:/test-volume-read-only:ro"], 174 ) 175 ctr.start() 176 177 buff: IO[bytes] = io.BytesIO() 178 with tarfile.open(fileobj=buff, mode="w:") as file: 179 info: tarfile.TarInfo = tarfile.TarInfo() 180 info.uid = 1042 181 info.gid = 1043 182 info.name = "a.txt" 183 info.path = "a.txt" 184 info.mode = 0o644 185 info.type = tarfile.REGTYPE 186 info.size = len(test_file_content) 187 file.addfile(info, fileobj=io.BytesIO(test_file_content)) 188 189 buff.seek(0) 190 ctr.put_archive("/tmp/", buff) 191 ret, out = ctr.exec_run(["stat", "-c", "%u:%g", "/tmp/a.txt"]) 192 193 self.assertEqual(ret, 0) 194 self.assertEqual(out.rstrip(), b"1042:1043", "UID/GID of copied file") 195 196 ret, out = ctr.exec_run(["cat", "/tmp/a.txt"]) 197 self.assertEqual(ret, 0) 198 self.assertEqual(out.rstrip(), test_file_content, "Content of copied file") 199 200 buff.seek(0) 201 with self.assertRaises(errors.APIError): 202 ctr.put_archive("/test-volume-read-only/", buff) 203 finally: 204 if ctr is not None: 205 ctr.stop() 206 ctr.remove() 207 if vol is not None: 208 vol.remove(force=True) 209 210 def test_mount_preexisting_dir(self): 211 dockerfile = ( 212 b"FROM quay.io/libpod/alpine:latest\n" 213 b"USER root\n" 214 b"RUN mkdir -p /workspace\n" 215 b"RUN chown 1042:1043 /workspace" 216 ) 217 img: Image 218 img, out = self.docker.images.build(fileobj=io.BytesIO(dockerfile)) 219 ctr: Container = self.docker.containers.create( 220 image=img.id, 221 detach=True, 222 command="top", 223 volumes=["test_mount_preexisting_dir_vol:/workspace"], 224 ) 225 ctr.start() 226 _, out = ctr.exec_run(["stat", "-c", "%u:%g", "/workspace"]) 227 self.assertEqual(out.rstrip(), b"1042:1043", "UID/GID set in dockerfile") 228 229 def test_non_existent_workdir(self): 230 dockerfile = ( 231 b"FROM quay.io/libpod/alpine:latest\n" 232 b"USER root\n" 233 b"WORKDIR /workspace/scratch\n" 234 b"RUN touch test" 235 ) 236 img: Image 237 img, _ = self.docker.images.build(fileobj=io.BytesIO(dockerfile)) 238 ctr: Container = self.docker.containers.create( 239 image=img.id, 240 detach=True, 241 command="top", 242 volumes=["test_non_existent_workdir:/workspace"], 243 ) 244 ctr.start() 245 ret, _ = ctr.exec_run(["stat", "/workspace/scratch/test"]) 246 self.assertEqual(ret, 0, "Working directory created if it doesn't exist") 247 248 def test_build_pull(self): 249 dockerfile = ( 250 b"FROM quay.io/libpod/alpine:latest\n" 251 b"USER 1000:1000\n" 252 ) 253 img: Image 254 img, logs = self.docker.images.build(fileobj=io.BytesIO(dockerfile), quiet=False, pull=True) 255 has_tried_pull = False 256 for e in logs: 257 if "stream" in e and "trying to pull" in e["stream"].lower(): 258 has_tried_pull = True 259 self.assertTrue(has_tried_pull, "the build process has not tried to pull the base image") 260 261 img, logs = self.docker.images.build(fileobj=io.BytesIO(dockerfile), quiet=False, pull=False) 262 has_tried_pull = False 263 for e in logs: 264 if "stream" in e and "trying to pull" in e["stream"].lower(): 265 has_tried_pull = True 266 self.assertFalse(has_tried_pull, "the build process has tried tried to pull the base image") 267 268 def test_mount_rw_by_default(self): 269 ctr: Optional[Container] = None 270 vol: Optional[Volume] = None 271 272 try: 273 vol = self.docker.volumes.create("test-volume") 274 ctr = self.docker.containers.create( 275 image="alpine", 276 detach=True, 277 command="top", 278 mounts=[ 279 Mount(target="/vol-mnt", source="test-volume", type="volume", read_only=False) 280 ], 281 ) 282 ctr_inspect = self.docker.api.inspect_container(ctr.id) 283 binds: List[str] = ctr_inspect["HostConfig"]["Binds"] 284 self.assertEqual(len(binds), 1) 285 self.assertEqual(binds[0], "test-volume:/vol-mnt:rw,rprivate,nosuid,nodev,rbind") 286 finally: 287 if ctr is not None: 288 ctr.remove() 289 if vol is not None: 290 vol.remove(force=True) 291 292 def test_wait_next_exit(self): 293 ctr: Container = self.docker.containers.create( 294 image=constant.ALPINE, 295 name="test-exit", 296 command=["true"], 297 labels={"my-label": "0" * 250_000}) 298 299 try: 300 def wait_and_start(): 301 time.sleep(5) 302 ctr.start() 303 304 t = threading.Thread(target=wait_and_start) 305 t.start() 306 ctr.wait(condition="next-exit", timeout=300) 307 t.join() 308 finally: 309 ctr.stop() 310 ctr.remove(force=True) 311 312 def test_container_inspect_compatibility(self): 313 """Test container inspect result compatibility with DOCKER_API. 314 When upgrading module "github.com/docker/docker" this test might fail, if so please correct podman inspect 315 command result to stay compatible with docker. 316 """ 317 ctr = self.docker.containers.create(image="alpine", detach=True) 318 try: 319 spec = yaml.load(open("vendor/github.com/docker/docker/api/swagger.yaml").read(), Loader=yaml.Loader) 320 ctr_inspect = json.loads(self.podman.run("inspect", ctr.id).stdout)[0] 321 schema = spec['paths']["/containers/{id}/json"]["get"]['responses'][200]['schema'] 322 schema["definitions"] = spec["definitions"] 323 324 OAS31Validator.check_schema(schema) 325 validator = OAS31Validator(schema) 326 important_error = [] 327 for error in validator.iter_errors(ctr_inspect): 328 if isinstance(error, ValidationError): 329 # ignore None instead of object/array/string errors 330 if error.message.startswith("None is not of type"): 331 continue 332 # ignore Windows specific option error 333 if error.json_path == '$.HostConfig.Isolation': 334 continue 335 important_error.append(error) 336 if important_error: 337 if newversion := spec["info"]["version"] != DOCKER_API_COMPATIBILITY_VERSION: 338 ex = Exception(f"There may be a breaking change in Docker API between " 339 f"{DOCKER_API_COMPATIBILITY_VERSION} and {newversion}") 340 raise best_match(important_error) from ex 341 else: 342 raise best_match(important_error) 343 finally: 344 ctr.stop() 345 ctr.remove(force=True)