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)