@@ -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,103 @@ 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 (f"PID for container { container .name } : { pid } - not killing." )
199169 except Exception as e2 :
200- if raise_error :
201- raise e2
202- log_error (
170+ raise Exception (
203171 f"Failed to forcefully kill container { container .name } : { e2 } \n "
204172 f"{ traceback .format_exc ()} "
205173 )
206174
207175 # Attempt to remove the container
208176 try :
209- log_info (f"Attempting to remove container { container .name } ..." )
177+ logger . info (f"Attempting to remove container { container .name } ..." )
210178 container .remove (force = True )
211- log_info (f"Container { container .name } removed." )
179+ logger . info (f"Container { container .name } removed." )
212180 except Exception as e :
213- if raise_error :
214- raise e
215- log_error (
181+ raise Exception (
216182 f"Failed to remove container { container .name } : { e } \n "
217183 f"{ traceback .format_exc ()} "
218184 )
219185
220186
187+ def image_exists_locally (client : docker .DockerClient , image_name : str , tag : str , logger : logging .Logger ) -> bool :
188+ """
189+ Check if a Docker image exists locally.
190+
191+ Args:
192+ ----
193+ client (docker.DockerClient): Docker client instance.
194+ image_name (str): The name of the Docker image.
195+ tag (str, optional): Tag of the Docker image.
196+ logger (logging.Logger): Logger instance.
197+
198+ Returns:
199+ -------
200+ bool: True if the image exists locally, False otherwise.
201+ """
202+ images = client .images .list (name = image_name )
203+ for image in images :
204+ if f"{ image_name } :{ tag } " in image .tags :
205+ logger .info (f"Using { image_name } :{ tag } found locally." )
206+ return True
207+ logger .info (f"{ image_name } :{ tag } cannot be found locally" )
208+ return False
209+
210+ def pull_image_from_docker_hub (client : docker .DockerClient , image_name : str , tag : str , logger : logging .Logger ) -> docker .models .images .Image :
211+ """
212+ Pull a Docker image from Docker Hub.
213+
214+ Args:
215+ ----
216+ client (docker.DockerClient): Docker client instance.
217+ image_name (str): The name of the Docker image.
218+ tag (str, optional): Tag of the Docker image.
219+ logger (logging.Logger): Logger instance.
220+
221+ Returns:
222+ -------
223+ docker.models.images.Image: The pulled Docker image.
224+
225+ Raises:
226+ ------
227+ docker.errors.ImageNotFound: If the image is not found on Docker Hub.
228+ docker.errors.APIError: If there's an issue with the Docker API during the pull.
229+ """
230+ try :
231+ image = client .images .pull (image_name , tag = tag )
232+ logger .info (f"Loaded { image_name } :{ tag } from Docker Hub." )
233+ return image
234+ except docker .errors .ImageNotFound :
235+ raise Exception (f"Image { image_name } :{ tag } not found on Docker Hub." )
236+ except docker .errors .APIError as e :
237+ raise Exception (f"Error pulling image: { e } " )
238+
239+
221240def create_container (
222241 client : docker .DockerClient ,
223242 image_name : str ,
224- container_name : Optional [str ] = None ,
243+ container_name : str ,
244+ logger : logging .Logger ,
225245 user : Optional [str ] = None ,
226246 command : Optional [str ] = "tail -f /dev/null" ,
227247 nano_cpus : Optional [int ] = None ,
228- logger : Optional [Union [str , logging .Logger ]] = None ,
229248) -> Container :
230249 """Start a Docker container using the specified image.
231250
232251 Args:
233252 ----
234253 client (docker.DockerClient): Docker client.
235254 image_name (str): The name of the Docker image.
236- container_name (str, optional): Name for the Docker container. Defaults to None.
255+ container_name (str): Name for the Docker container.
256+ logger (logging.Logger): Logger instance or log level as string for logging container creation messages.
237257 user (str, option): Log in as which user. Defaults to None.
238258 command (str, optional): Command to run in the container. Defaults to None.
239259 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.
241260
242261 Returns:
243262 -------
@@ -249,41 +268,13 @@ def create_container(
249268 Exception: For other general errors.
250269
251270 """
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 )
271+ image , tag = image_name .split (":" )
272+ if not image_exists_locally (client , image , tag , logger ):
273+ pull_image_from_docker_hub (client , image , tag , logger )
283274
284275 container = None
285276 try :
286- log_info (f"Creating container for { image_name } ..." )
277+ logger . info (f"Creating container for { image_name } ..." )
287278 container = client .containers .run (
288279 image = image_name ,
289280 name = container_name ,
@@ -292,12 +283,12 @@ def log_info(x: str) -> None:
292283 nano_cpus = nano_cpus ,
293284 detach = True ,
294285 )
295- log_info (f"Container for { image_name } created: { container .id } " )
286+ logger . info (f"Container for { image_name } created: { container .id } " )
296287 return container
297288 except Exception as e :
298289 # 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 ())
290+ logger . error (f"Error creating container for { image_name } : { e } " )
291+ logger . info (traceback .format_exc ())
301292 assert container is not None
302293 cleanup_container (client , container , logger )
303294 raise
0 commit comments