Skip to content

Commit 3894eab

Browse files
authored
Merge pull request #41 from commit-0/docker-image
Docker image
2 parents 13a68c9 + 4bdbb83 commit 3894eab

File tree

4 files changed

+99
-90
lines changed

4 files changed

+99
-90
lines changed

commit0/harness/build.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ def main(
3131

3232
client = docker.from_env()
3333
build_repo_images(client, specs, num_workers)
34+
for spec in specs:
35+
image = client.images.get(spec.repo_image_key)
36+
repository, tag = spec.repo_image_tag.split(":")
37+
image.tag(repository, tag)
3438

3539

3640
__all__ = []

commit0/harness/docker_utils.py

Lines changed: 81 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import traceback
1111
from pathlib import Path
1212
from io import BytesIO
13-
from typing import Optional, List, Union
13+
from typing import Optional, List
1414

1515
import docker.errors
1616
from docker.models.containers import Container
@@ -126,7 +126,7 @@ def write_to_container(container: Container, data: str, dst: Path) -> None:
126126
def 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+
221246
def 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

commit0/harness/execution_context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def __init__(
9999
self.client = docker.from_env()
100100
self.container = create_container(
101101
client=self.client,
102-
image_name=spec.repo_image_key,
102+
image_name=spec.repo_image_tag,
103103
container_name=spec.get_container_name(),
104104
nano_cpus=num_cpus,
105105
logger=logger,

commit0/harness/spec.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import hashlib
12
from dataclasses import dataclass
23
from typing import Union, cast, Optional
34

@@ -46,12 +47,19 @@ def repo_image_key(self) -> str:
4647
4748
Note that old images are not automatically deleted, so consider cleaning up old images periodically.
4849
"""
49-
# hash_object = hashlib.sha256()
50-
# hash_object.update(str(self.setup_script).encode("utf-8"))
51-
# hash_value = hash_object.hexdigest()
52-
# val = hash_value[:22] # 22 characters is still very likely to be unique
50+
hash_object = hashlib.sha256()
51+
hash_object.update(str(self.setup_script).encode("utf-8"))
52+
hash_value = hash_object.hexdigest()
53+
val = hash_value[:22] # 22 characters is still very likely to be unique
54+
repo = self.repo.split("/")[-1]
55+
# this is the image name created locally
56+
# once this image created, it will be tagged with repo_image_tag
57+
return f"commit0.repo.{repo}.{val}:latest".lower()
58+
59+
@property
60+
def repo_image_tag(self) -> str:
61+
"""Repo image tag that will be used throughout."""
5362
repo = self.repo.split("/")[-1]
54-
# return f"commit0.repo.{repo}.{val}:latest".lower()
5563
return f"wentingzhao/{repo}:latest".lower()
5664

5765
def get_container_name(self, run_id: Optional[str] = None) -> str:

0 commit comments

Comments
 (0)