10
10
import traceback
11
11
from pathlib import Path
12
12
from io import BytesIO
13
- from typing import Optional , List , Union
13
+ from typing import Optional , List
14
14
15
15
import docker .errors
16
16
from docker .models .containers import Container
@@ -126,7 +126,7 @@ def write_to_container(container: Container, data: str, dst: Path) -> None:
126
126
def cleanup_container (
127
127
client : docker .DockerClient ,
128
128
container : Container ,
129
- logger : Union [ None , str , logging .Logger ] ,
129
+ logger : logging .Logger ,
130
130
) -> None :
131
131
"""Stop and remove a Docker container.
132
132
Performs this forcefully if the container cannot be stopped with the python API.
@@ -135,51 +135,21 @@ def cleanup_container(
135
135
----
136
136
client (docker.DockerClient): Docker client.
137
137
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.
139
139
140
140
"""
141
141
if not container :
142
142
return
143
143
144
144
container_id = container .id
145
145
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
-
176
146
# Attempt to stop the container
177
147
try :
178
148
if container :
179
- log_info (f"Attempting to stop container { container .name } ..." )
149
+ logger . info (f"Attempting to stop container { container .name } ..." )
180
150
container .kill ()
181
151
except Exception as e :
182
- log_error (
152
+ logger . error (
183
153
f"Failed to stop container { container .name } : { e } . Trying to forcefully kill..."
184
154
)
185
155
try :
@@ -190,54 +160,109 @@ def log_info(x: str) -> None:
190
160
191
161
# If container PID found, forcefully kill the container
192
162
if pid > 0 :
193
- log_info (
163
+ logger . info (
194
164
f"Forcefully killing container { container .name } with PID { pid } ..."
195
165
)
196
166
os .kill (pid , signal .SIGKILL )
197
167
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
+ )
199
171
except Exception as e2 :
200
- if raise_error :
201
- raise e2
202
- log_error (
172
+ raise Exception (
203
173
f"Failed to forcefully kill container { container .name } : { e2 } \n "
204
174
f"{ traceback .format_exc ()} "
205
175
)
206
176
207
177
# Attempt to remove the container
208
178
try :
209
- log_info (f"Attempting to remove container { container .name } ..." )
179
+ logger . info (f"Attempting to remove container { container .name } ..." )
210
180
container .remove (force = True )
211
- log_info (f"Container { container .name } removed." )
181
+ logger . info (f"Container { container .name } removed." )
212
182
except Exception as e :
213
- if raise_error :
214
- raise e
215
- log_error (
183
+ raise Exception (
216
184
f"Failed to remove container { container .name } : { e } \n "
217
185
f"{ traceback .format_exc ()} "
218
186
)
219
187
220
188
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
+
221
246
def create_container (
222
247
client : docker .DockerClient ,
223
248
image_name : str ,
224
- container_name : Optional [str ] = None ,
249
+ container_name : str ,
250
+ logger : logging .Logger ,
225
251
user : Optional [str ] = None ,
226
252
command : Optional [str ] = "tail -f /dev/null" ,
227
253
nano_cpus : Optional [int ] = None ,
228
- logger : Optional [Union [str , logging .Logger ]] = None ,
229
254
) -> Container :
230
255
"""Start a Docker container using the specified image.
231
256
232
257
Args:
233
258
----
234
259
client (docker.DockerClient): Docker client.
235
260
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.
237
263
user (str, option): Log in as which user. Defaults to None.
238
264
command (str, optional): Command to run in the container. Defaults to None.
239
265
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.
241
266
242
267
Returns:
243
268
-------
@@ -249,41 +274,13 @@ def create_container(
249
274
Exception: For other general errors.
250
275
251
276
"""
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 )
283
280
284
281
container = None
285
282
try :
286
- log_info (f"Creating container for { image_name } ..." )
283
+ logger . info (f"Creating container for { image_name } ..." )
287
284
container = client .containers .run (
288
285
image = image_name ,
289
286
name = container_name ,
@@ -292,12 +289,12 @@ def log_info(x: str) -> None:
292
289
nano_cpus = nano_cpus ,
293
290
detach = True ,
294
291
)
295
- log_info (f"Container for { image_name } created: { container .id } " )
292
+ logger . info (f"Container for { image_name } created: { container .id } " )
296
293
return container
297
294
except Exception as e :
298
295
# 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 ())
301
298
assert container is not None
302
299
cleanup_container (client , container , logger )
303
300
raise
0 commit comments