Coverage for bugscpp/processor/core/docker.py : 77%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Manage commands associated with docker SDK module.
4Do not use docker SDK directly, instead use Docker class.
5"""
6import os
7import sys
8from pathlib import Path
9from textwrap import dedent
10from typing import Dict, Optional, cast
12import docker.errors
13from config import config
14from docker import DockerClient
15from docker.models.containers import Container, ExecResult
16from docker.models.images import Image
17from errors import DppError
18from errors.docker import (DppDockerBuildClientError, DppDockerBuildError, DppDockerBuildServerError,
19 DppDockerNoClientError)
20from message import message
21from processor.core.data import Worktree
24def _cast_image(image) -> Image:
25 """
26 Helper function to get a correct type
27 """
28 return cast(Image, image)
31def _cast_container(container) -> Container:
32 """
33 Helper function to get a correct type
34 """
35 return cast(Container, container)
38def _try_build_image(client, tag, path, verbose) -> Image:
39 try:
40 image, stream = client.images.build(rm=True, tag=tag, path=path, pull=True)
41 if verbose:
42 for chunk in stream:
43 if "stream" in chunk:
44 for line in chunk["stream"].splitlines():
45 print(line)
46 return image
47 except docker.errors.BuildError as e:
48 raise DppDockerBuildError(e.msg)
49 except docker.errors.APIError as e:
50 if e.is_client_error():
51 raise DppDockerBuildClientError(e.explanation)
52 else:
53 raise DppDockerBuildServerError(e.explanation)
56def _build_image(client, tag, path, verbose) -> Image:
57 """
58 Helper function to get a correct type
59 """
60 try:
61 return _try_build_image(client, tag, path, verbose)
62 except DppError as e:
63 message.stdout_progress_error(e)
64 sys.exit(1)
67class _Client:
68 def __get__(self, instance, owner) -> DockerClient:
69 if getattr(owner, "_client", None) is None:
70 try:
71 setattr(owner, "_client", docker.from_env())
72 except docker.errors.DockerException:
73 message.error(__name__, "no response from docker-daemon")
74 raise DppDockerNoClientError()
75 return getattr(owner, "_client")
78class Docker:
79 """
80 Provide docker SDK methods along with context manager.
81 It is recommended to use this via `with` statement.
83 Examples
84 --------
85 >>> with Docker("/path/to/Dockerfile", my_worktree) as docker:
86 ... docker.send("echo 'Hello, world!'")
88 Notes
89 -----
90 Host machine must be running docker daemon in background.
91 """
93 client = _Client()
94 """Docker SDK client."""
96 def __init__(
97 self,
98 dockerfile: str,
99 worktree: Worktree,
100 environ: Optional[Dict[str, str]] = None,
101 rebuild_image=False,
102 user=None,
103 uid_of_user: Optional[str] = None,
104 verbose=True,
105 ):
106 self._dockerfile = dockerfile
107 # Assume that the parent directory has the same name as the target.
108 tag = Path(dockerfile).parent.name
109 self._container_name: str = f"{tag}-dpp"
110 self._tag = f"hschoe/defects4cpp-ubuntu:{tag}"
111 self._volume: Dict[str, Dict] = {
112 str(worktree.host.resolve()): {
113 "bind": str(worktree.container),
114 "mode": "rw",
115 }
116 }
117 self._working_dir: str = str(worktree.container)
118 self._environ = environ
119 self._rebuild_image = rebuild_image
120 self._image: Optional[Image] = None
121 self._container: Optional[Container] = None
122 self._user = user
123 self._uid_of_dpp_docker_user = uid_of_user
124 self._verbose = verbose
126 @property
127 def image(self) -> Image:
128 """Docker SDK Image."""
129 if not self._image:
130 message.stdout_progress_detail(f" Image: {self._tag}")
131 try:
132 # SET DEFECTS4CPP_TEST_TAXONOMY to 1 for taxonomy testing
133 if os.environ.get("DEFECTS4CPP_TEST_TAXONOMY", "0") not in ["YES", "1"]:
134 self.client.images.pull(self._tag)
135 self._image = _cast_image(self.client.images.get(self._tag))
136 except docker.errors.ImageNotFound:
137 message.info(
138 f"ImageNotFound {self.client}, {self._tag}, {str(Path(self._dockerfile).parent)}"
139 )
140 self._image = _build_image(
141 self.client,
142 self._tag,
143 str(Path(self._dockerfile).parent),
144 self._verbose,
145 )
146 except docker.errors.APIError as api_error:
147 message.stdout_error(
148 f" An API Error occured.{os.linesep}"
149 f" Find detailed message at {message.path}."
150 )
151 message.error(__name__, f"APIError {api_error}")
152 self._image = _build_image(
153 self.client,
154 self._tag,
155 str(Path(self._dockerfile).parent),
156 self._verbose,
157 )
159 if self._uid_of_dpp_docker_user is not None:
160 # set uid of user(defects4cpp) to self._uid_of_dpp_docker_user after checking if uid need to be changed
161 _container = _cast_container(
162 self.client.containers.run(
163 self._image,
164 detach=True,
165 stdin_open=True,
166 environment=self._environ,
167 name=self._container_name,
168 )
169 )
170 _, output = _container.exec_run(f"id -u {config.DPP_DOCKER_USER}")
171 uid = str(output, "utf-8").strip("\n")
172 if self._uid_of_dpp_docker_user != uid:
173 message.stdout_progress_detail(
174 f" Setting uid of {config.DPP_DOCKER_USER} "
175 f"from {uid} "
176 f"to {self._uid_of_dpp_docker_user}."
177 )
178 _, output = _container.exec_run(
179 f"usermod -u {self._uid_of_dpp_docker_user} {config.DPP_DOCKER_USER}"
180 )
181 repository, tag = tuple(self._tag.split(":"))
182 self._image = _cast_image(
183 _container.commit(repository=repository, tag=tag)
184 )
185 _container.stop()
186 _container.remove()
187 return self._image
189 def __enter__(self):
190 k = list(self._volume)[0]
191 message.info(
192 __name__,
193 dedent(
194 """container.__enter__ ({})
195 - from {}
196 - to {}
197 - mode {}""".format(
198 self._container_name,
199 DppError.print_path(k),
200 self._volume[k]["bind"],
201 self._volume[k]["mode"],
202 )
203 ),
204 )
205 message.stdout_progress_detail("Starting container")
206 self._container = _cast_container(
207 self.client.containers.run(
208 self.image,
209 auto_remove=True,
210 command="/bin/sh",
211 detach=True,
212 environment=self._environ,
213 name=self._container_name,
214 stdin_open=True,
215 user=config.DPP_DOCKER_USER if not self._user else self._user,
216 volumes=self._volume,
217 working_dir=self._working_dir,
218 )
219 )
220 return self
222 def __exit__(self, type, value, traceback):
223 message.info(__name__, f"container.__exit__ ({self._container_name})")
224 message.stdout_progress_detail("Closing container")
225 self._container.stop()
227 def send(self, command: str, stream=True, **kwargs) -> ExecResult:
228 """
229 Send a single line command to the running container.
230 """
231 return self._container.exec_run(command, stream=stream, **kwargs)