1010import traceback
1111from pathlib import Path
1212from io import BytesIO
13- from typing import Optional , List , Union
13+ from typing import Optional , List
1414
1515import docker .errors
1616from docker .models .containers import Container
@@ -126,7 +126,7 @@ def write_to_container(container: Container, data: str, dst: Path) -> None:
126126def cleanup_container (
127127 client : docker .DockerClient ,
128128 container : Container ,
129- logger : Union [ None , str , logging .Logger ] ,
129+ logger : logging .Logger ,
130130) -> None :
131131 """Stop and remove a Docker container.
132132 Performs this forcefully if the container cannot be stopped with the python API.
@@ -135,51 +135,21 @@ def cleanup_container(
135135 ----
136136 client (docker.DockerClient): Docker client.
137137 container (docker.Container): Container to remove.
138- logger (Union[str, logging.Logger], optional ): Logger instance or log level as string for logging container creation messages. Defaults to None .
138+ logger (logging.Logger): Logger instance or log level as string for logging container creation messages.
139139
140140 """
141141 if not container :
142142 return
143143
144144 container_id = container .id
145145
146- if not logger :
147- # if logger is None, print to stdout
148- def log_error (x : str ) -> None :
149- print (x )
150-
151- def log_info (x : str ) -> None :
152- print (x )
153-
154- raise_error = True
155- elif logger == "quiet" :
156- # if logger is "quiet", don't print anything
157- def log_info (x : str ) -> None :
158- return None
159-
160- def log_error (x : str ) -> None :
161- return None
162-
163- raise_error = True
164- else :
165- assert isinstance (logger , logging .Logger )
166-
167- # if logger is a logger object, use it
168- def log_error (x : str ) -> None :
169- logger .info (x )
170-
171- def log_info (x : str ) -> None :
172- logger .info (x )
173-
174- raise_error = False
175-
176146 # Attempt to stop the container
177147 try :
178148 if container :
179- log_info (f"Attempting to stop container { container .name } ..." )
149+ logger . info (f"Attempting to stop container { container .name } ..." )
180150 container .kill ()
181151 except Exception as e :
182- log_error (
152+ logger . error (
183153 f"Failed to stop container { container .name } : { e } . Trying to forcefully kill..."
184154 )
185155 try :
@@ -190,54 +160,109 @@ def log_info(x: str) -> None:
190160
191161 # If container PID found, forcefully kill the container
192162 if pid > 0 :
193- log_info (
163+ logger . info (
194164 f"Forcefully killing container { container .name } with PID { pid } ..."
195165 )
196166 os .kill (pid , signal .SIGKILL )
197167 else :
198- log_error (f"PID for container { container .name } : { pid } - not killing." )
168+ logger .error (
169+ f"PID for container { container .name } : { pid } - not killing."
170+ )
199171 except Exception as e2 :
200- if raise_error :
201- raise e2
202- log_error (
172+ raise Exception (
203173 f"Failed to forcefully kill container { container .name } : { e2 } \n "
204174 f"{ traceback .format_exc ()} "
205175 )
206176
207177 # Attempt to remove the container
208178 try :
209- log_info (f"Attempting to remove container { container .name } ..." )
179+ logger . info (f"Attempting to remove container { container .name } ..." )
210180 container .remove (force = True )
211- log_info (f"Container { container .name } removed." )
181+ logger . info (f"Container { container .name } removed." )
212182 except Exception as e :
213- if raise_error :
214- raise e
215- log_error (
183+ raise Exception (
216184 f"Failed to remove container { container .name } : { e } \n "
217185 f"{ traceback .format_exc ()} "
218186 )
219187
220188
189+ def image_exists_locally (
190+ client : docker .DockerClient , image_name : str , tag : str , logger : logging .Logger
191+ ) -> bool :
192+ """Check if a Docker image exists locally.
193+
194+ Args:
195+ ----
196+ client (docker.DockerClient): Docker client instance.
197+ image_name (str): The name of the Docker image.
198+ tag (str, optional): Tag of the Docker image.
199+ logger (logging.Logger): Logger instance.
200+
201+ Returns:
202+ -------
203+ bool: True if the image exists locally, False otherwise.
204+
205+ """
206+ images = client .images .list (name = image_name )
207+ for image in images :
208+ if f"{ image_name } :{ tag } " in image .tags :
209+ logger .info (f"Using { image_name } :{ tag } found locally." )
210+ return True
211+ logger .info (f"{ image_name } :{ tag } cannot be found locally" )
212+ return False
213+
214+
215+ def pull_image_from_docker_hub (
216+ client : docker .DockerClient , image_name : str , tag : str , logger : logging .Logger
217+ ) -> None :
218+ """Pull a Docker image from Docker Hub.
219+
220+ Args:
221+ ----
222+ client (docker.DockerClient): Docker client instance.
223+ image_name (str): The name of the Docker image.
224+ tag (str, optional): Tag of the Docker image.
225+ logger (logging.Logger): Logger instance.
226+
227+ Returns:
228+ -------
229+ docker.models.images.Image: The pulled Docker image.
230+
231+ Raises:
232+ ------
233+ docker.errors.ImageNotFound: If the image is not found on Docker Hub.
234+ docker.errors.APIError: If there's an issue with the Docker API during the pull.
235+
236+ """
237+ try :
238+ client .images .pull (image_name , tag = tag )
239+ logger .info (f"Loaded { image_name } :{ tag } from Docker Hub." )
240+ except docker .errors .ImageNotFound :
241+ raise Exception (f"Image { image_name } :{ tag } not found on Docker Hub." )
242+ except docker .errors .APIError as e :
243+ raise Exception (f"Error pulling image: { e } " )
244+
245+
221246def create_container (
222247 client : docker .DockerClient ,
223248 image_name : str ,
224- container_name : Optional [str ] = None ,
249+ container_name : str ,
250+ logger : logging .Logger ,
225251 user : Optional [str ] = None ,
226252 command : Optional [str ] = "tail -f /dev/null" ,
227253 nano_cpus : Optional [int ] = None ,
228- logger : Optional [Union [str , logging .Logger ]] = None ,
229254) -> Container :
230255 """Start a Docker container using the specified image.
231256
232257 Args:
233258 ----
234259 client (docker.DockerClient): Docker client.
235260 image_name (str): The name of the Docker image.
236- container_name (str, optional): Name for the Docker container. Defaults to None.
261+ container_name (str): Name for the Docker container.
262+ logger (logging.Logger): Logger instance or log level as string for logging container creation messages.
237263 user (str, option): Log in as which user. Defaults to None.
238264 command (str, optional): Command to run in the container. Defaults to None.
239265 nano_cpus (int, optional): The number of CPUs for the container. Defaults to None.
240- logger (Union[str, logging.Logger], optional): Logger instance or log level as string for logging container creation messages. Defaults to None.
241266
242267 Returns:
243268 -------
@@ -249,41 +274,13 @@ def create_container(
249274 Exception: For other general errors.
250275
251276 """
252- try :
253- # Pull the image if it doesn't already exist
254- client .images .pull (image_name )
255- except docker .errors .APIError as e :
256- raise docker .errors .APIError (f"Error pulling image: { str (e )} " )
257-
258- if not logger :
259- # if logger is None, print to stdout
260- def log_error (x : str ) -> None :
261- print (x )
262-
263- def log_info (x : str ) -> None :
264- print (x )
265-
266- elif logger == "quiet" :
267- # if logger is "quiet", don't print anything
268- def log_info (x : str ) -> None :
269- return None
270-
271- def log_error (x : str ) -> None :
272- return None
273-
274- else :
275- assert isinstance (logger , logging .Logger )
276-
277- # if logger is a logger object, use it
278- def log_error (x : str ) -> None :
279- logger .info (x )
280-
281- def log_info (x : str ) -> None :
282- logger .info (x )
277+ image , tag = image_name .split (":" )
278+ if not image_exists_locally (client , image , tag , logger ):
279+ pull_image_from_docker_hub (client , image , tag , logger )
283280
284281 container = None
285282 try :
286- log_info (f"Creating container for { image_name } ..." )
283+ logger . info (f"Creating container for { image_name } ..." )
287284 container = client .containers .run (
288285 image = image_name ,
289286 name = container_name ,
@@ -292,12 +289,12 @@ def log_info(x: str) -> None:
292289 nano_cpus = nano_cpus ,
293290 detach = True ,
294291 )
295- log_info (f"Container for { image_name } created: { container .id } " )
292+ logger . info (f"Container for { image_name } created: { container .id } " )
296293 return container
297294 except Exception as e :
298295 # If an error occurs, clean up the container and raise an exception
299- log_error (f"Error creating container for { image_name } : { e } " )
300- log_info (traceback .format_exc ())
296+ logger . error (f"Error creating container for { image_name } : { e } " )
297+ logger . info (traceback .format_exc ())
301298 assert container is not None
302299 cleanup_container (client , container , logger )
303300 raise
0 commit comments