def exec_in_container( self, container_name_or_id: str, command: Union[List[str], str], interactive=False, detach=False, env_vars: Optional[Dict[str, Optional[str]]] = None, stdin: Optional[bytes] = None, user: Optional[str] = None, workdir: Optional[str] = None, ) -> Tuple[bytes, bytes]: env_file = None cmd = self._docker_cmd() cmd.append("exec") if interactive: cmd.append("--interactive") if detach: cmd.append("--detach") if user: cmd += ["--user", user] if workdir: cmd += ["--workdir", workdir] if env_vars: env_flag, env_file = Util.create_env_vars_file_flag(env_vars) cmd += env_flag cmd.append(container_name_or_id) cmd += command if isinstance(command, List) else [command] LOG.debug("Execute in container cmd: %s", cmd) result = self._run_async_cmd(cmd, stdin, container_name_or_id) Util.rm_env_vars_file(env_file) return result
def run_container(self, image_name: str, stdin=None, **kwargs) -> Tuple[bytes, bytes]: cmd, env_file = self._build_run_create_cmd("run", image_name, **kwargs) LOG.debug("Run container with cmd: %s", cmd) result = self._run_async_cmd(cmd, stdin, kwargs.get("name") or "", image_name) Util.rm_env_vars_file(env_file) return result
def get_docker_image_names(self, strip_latest=True, include_tags=True): try: images = self.client().images.list() image_names = [tag for image in images for tag in image.tags if image.tags] if not include_tags: image_names = list(map(lambda image_name: image_name.split(":")[0], image_names)) if strip_latest: Util.append_without_latest(image_names) return image_names except APIError as e: raise ContainerException() from e
def get_docker_image_names(self, strip_latest=True, include_tags=True): format_string = "{{.Repository}}:{{.Tag}}" if include_tags else "{{.Repository}}" cmd = self._docker_cmd() cmd += ["images", "--format", format_string] try: output = run(cmd) image_names = output.splitlines() if strip_latest: Util.append_without_latest(image_names) return image_names except Exception as e: LOG.info('Unable to list Docker images via "%s": %s', cmd, e) return []
def copy_from_container( self, container_name: str, local_path: str, container_path: str, ) -> None: LOG.debug("Copying file from %s:%s to %s", container_name, container_path, local_path) try: container = self.client().containers.get(container_name) bits, _ = container.get_archive(container_path) Util.untar_to_path(bits, local_path) except NotFound: raise NoSuchContainer(container_name) except APIError as e: raise ContainerException() from e
def create_container(self, image_name: str, **kwargs) -> str: cmd, env_file = self._build_run_create_cmd("create", image_name, **kwargs) LOG.debug("Create container with cmd: %s", cmd) try: container_id = run(cmd) # Note: strip off Docker warning messages like "DNS setting (--dns=127.0.0.1) may fail in containers" container_id = container_id.strip().split("\n")[-1] return container_id.strip() except subprocess.CalledProcessError as e: if "Unable to find image" in to_str(e.stdout): raise NoSuchImage(image_name, stdout=e.stdout, stderr=e.stderr) raise ContainerException( "Docker process returned with errorcode %s" % e.returncode, e.stdout, e.stderr ) finally: Util.rm_env_vars_file(env_file)
def build_image(self, dockerfile_path: str, image_name: str, context_path: str = None): cmd = self._docker_cmd() dockerfile_path = Util.resolve_dockerfile_path(dockerfile_path) context_path = context_path or os.path.dirname(dockerfile_path) cmd += ["build", "-t", image_name, "-f", dockerfile_path, context_path] LOG.debug("Building Docker image: %s", cmd) try: run(cmd) except subprocess.CalledProcessError as e: raise ContainerException( f"Docker build process returned with error code {e.returncode}", e.stdout, e.stderr ) from e
def build_image(self, dockerfile_path: str, image_name: str, context_path: str = None): try: dockerfile_path = Util.resolve_dockerfile_path(dockerfile_path) context_path = context_path or os.path.dirname(dockerfile_path) LOG.debug("Building Docker image %s from %s", image_name, dockerfile_path) self.client().images.build( path=context_path, dockerfile=dockerfile_path, tag=image_name, rm=True, ) except APIError as e: raise ContainerException("Unable to build Docker image") from e
def copy_into_container( self, container_name: str, local_path: str, container_path: str ) -> None: # TODO behave like https://docs.docker.com/engine/reference/commandline/cp/ LOG.debug("Copying file %s into %s:%s", local_path, container_name, container_path) try: container = self.client().containers.get(container_name) target_exists, target_isdir = self._container_path_info(container, container_path) target_path = container_path if target_isdir else os.path.dirname(container_path) with Util.tar_path(local_path, container_path, is_dir=target_isdir) as tar: container.put_archive(target_path, tar) except NotFound: raise NoSuchContainer(container_name) except APIError as e: raise ContainerException() from e
def _build_run_create_cmd( self, action: str, image_name: str, *, name: Optional[str] = None, entrypoint: Optional[str] = None, remove: bool = False, interactive: bool = False, tty: bool = False, detach: bool = False, command: Optional[Union[List[str], str]] = None, mount_volumes: Optional[List[SimpleVolumeBind]] = None, ports: Optional[PortMappings] = None, env_vars: Optional[Dict[str, str]] = None, user: Optional[str] = None, cap_add: Optional[List[str]] = None, cap_drop: Optional[List[str]] = None, network: Optional[str] = None, dns: Optional[str] = None, additional_flags: Optional[str] = None, workdir: Optional[str] = None, ) -> Tuple[List[str], str]: env_file = None cmd = self._docker_cmd() + [action] if remove: cmd.append("--rm") if name: cmd += ["--name", name] if entrypoint is not None: # empty string entrypoint can be intentional cmd += ["--entrypoint", entrypoint] if mount_volumes: cmd += [ volume for host_path, docker_path in dict(mount_volumes).items() for volume in ["-v", f"{host_path}:{docker_path}"] ] if interactive: cmd.append("--interactive") if tty: cmd.append("--tty") if detach: cmd.append("--detach") if ports: cmd += ports.to_list() if env_vars: env_flags, env_file = Util.create_env_vars_file_flag(env_vars) cmd += env_flags if user: cmd += ["--user", user] if cap_add: cmd += list( itertools.chain.from_iterable(["--cap-add", cap] for cap in cap_add)) if cap_drop: cmd += list( itertools.chain.from_iterable(["--cap-drop", cap] for cap in cap_drop)) if network: cmd += ["--network", network] if dns: cmd += ["--dns", dns] if workdir: cmd += ["--workdir", workdir] if additional_flags: cmd += shlex.split(additional_flags) cmd.append(image_name) if command: cmd += command if isinstance(command, List) else [command] return cmd, env_file
def create_container( self, image_name: str, *, name: Optional[str] = None, entrypoint: Optional[str] = None, remove: bool = False, interactive: bool = False, tty: bool = False, detach: bool = False, command: Optional[Union[List[str], str]] = None, mount_volumes: Optional[List[SimpleVolumeBind]] = None, ports: Optional[PortMappings] = None, env_vars: Optional[Dict[str, str]] = None, user: Optional[str] = None, cap_add: Optional[List[str]] = None, cap_drop: Optional[List[str]] = None, security_opt: Optional[List[str]] = None, network: Optional[str] = None, dns: Optional[str] = None, additional_flags: Optional[str] = None, workdir: Optional[str] = None, ) -> str: LOG.debug("Creating container with attributes: %s", locals()) extra_hosts = None if additional_flags: env_vars, ports, mount_volumes, extra_hosts, network = Util.parse_additional_flags( additional_flags, env_vars, ports, mount_volumes, network ) try: kwargs = {} if cap_add: kwargs["cap_add"] = cap_add if cap_drop: kwargs["cap_drop"] = cap_drop if security_opt: kwargs["security_opt"] = security_opt if dns: kwargs["dns"] = [dns] if ports: kwargs["ports"] = ports.to_dict() if workdir: kwargs["working_dir"] = workdir mounts = None if mount_volumes: mounts = Util.convert_mount_list_to_dict(mount_volumes) def create_container(): return self.client().containers.create( image=image_name, command=command, auto_remove=remove, name=name, stdin_open=interactive, tty=tty, entrypoint=entrypoint, environment=env_vars, detach=detach, user=user, network=network, volumes=mounts, extra_hosts=extra_hosts, **kwargs, ) try: container = create_container() except ImageNotFound: self.pull_image(image_name) container = create_container() return container.id except ImageNotFound: raise NoSuchImage(image_name) except APIError as e: raise ContainerException() from e
def test_argument_parsing(): test_port_string = "-p 80:8080/udp" test_port_string_with_host = "-p 127.0.0.1:6000:7000/tcp" test_port_string_many_to_one = "-p 9230-9231:9230" test_env_string = "-e TEST_ENV_VAR=test_string=123" test_mount_string = "-v /var/test:/opt/test" argument_string = f"{test_port_string} {test_env_string} {test_mount_string} {test_port_string_with_host} {test_port_string_many_to_one}" env_vars = {} ports = PortMappings() mounts = [] Util.parse_additional_flags(argument_string, env_vars, ports, mounts) assert env_vars == {"TEST_ENV_VAR": "test_string=123"} assert ports.to_str() == "-p 80:8080/udp -p 6000:7000 -p 9230-9231:9230" assert mounts == [("/var/test", "/opt/test")] argument_string = ( "--add-host host.docker.internal:host-gateway --add-host arbitrary.host:127.0.0.1" ) _, _, _, extra_hosts, _ = Util.parse_additional_flags( argument_string, env_vars, ports, mounts) assert { "host.docker.internal": "host-gateway", "arbitrary.host": "127.0.0.1" } == extra_hosts with pytest.raises(NotImplementedError): argument_string = "--somerandomargument" Util.parse_additional_flags(argument_string, env_vars, ports, mounts) with pytest.raises(ValueError): argument_string = "--publish 80:80:80:80" Util.parse_additional_flags(argument_string, env_vars, ports, mounts) # Test windows paths argument_string = r'-v "C:\Users\SomeUser\SomePath:/var/task"' _, _, mounts, _, _ = Util.parse_additional_flags(argument_string) assert mounts == [(r"C:\Users\SomeUser\SomePath", "/var/task")] argument_string = r'-v "C:\Users\SomeUser\SomePath:/var/task:ro"' _, _, mounts, _, _ = Util.parse_additional_flags(argument_string) assert mounts == [(r"C:\Users\SomeUser\SomePath", "/var/task")] argument_string = r'-v "C:\Users\Some User\Some Path:/var/task:ro"' _, _, mounts, _, _ = Util.parse_additional_flags(argument_string) assert mounts == [(r"C:\Users\Some User\Some Path", "/var/task")] argument_string = r'-v "/var/test:/var/task:ro"' _, _, mounts, _, _ = Util.parse_additional_flags(argument_string) assert mounts == [("/var/test", "/var/task")] # Test file paths argument_string = r'-v "/tmp/test.jar:/tmp/foo bar/test.jar"' _, _, mounts, _, _ = Util.parse_additional_flags(argument_string) assert mounts == [(r"/tmp/test.jar", "/tmp/foo bar/test.jar")] argument_string = r'-v "/tmp/test-foo_bar.jar:/tmp/test-foo_bar2.jar"' _, _, mounts, _, _ = Util.parse_additional_flags(argument_string) assert mounts == [(r"/tmp/test-foo_bar.jar", "/tmp/test-foo_bar2.jar")] # Test file paths argument_string = r'-v "/tmp/test.jar:/tmp/foo bar/test.jar" --network mynet123' _, _, _, _, network = Util.parse_additional_flags(argument_string) assert network == "mynet123"