Skip to content

Commit 9b1f59a

Browse files
Arch: refactor and add unit tests for EventStreamRuntime docker image build (All-Hands-AI#2908)
* deprecating recall action * fix integration tests * fix integration tests * refractor runtime to use async * remove search memory * rename .initialize to .ainit * draft of runtime image building (separate from img agnostic) * refractor runtime build into separate file and add unit tests for it * fix image agnostic tests * Update opendevin/runtime/utils/runtime_build.py Co-authored-by: Mingzhang Zheng <649940882@qq.com> --------- Co-authored-by: Mingzhang Zheng <649940882@qq.com>
1 parent 959d21c commit 9b1f59a

File tree

6 files changed

+469
-204
lines changed

6 files changed

+469
-204
lines changed

opendevin/runtime/client/runtime.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
)
3434
from opendevin.runtime.runtime import Runtime
3535
from opendevin.runtime.utils import find_available_tcp_port
36-
from opendevin.runtime.utils.image_agnostic import get_od_sandbox_image
36+
from opendevin.runtime.utils.runtime_build import build_runtime_image
3737

3838

3939
class EventStreamRuntime(Runtime):
@@ -72,8 +72,13 @@ def __init__(
7272
self.action_semaphore = asyncio.Semaphore(1) # Ensure one action at a time
7373

7474
async def ainit(self):
75-
self.container_image = get_od_sandbox_image(
76-
self.container_image, self.docker_client, is_eventstream_runtime=True
75+
self.container_image = build_runtime_image(
76+
self.container_image,
77+
self.docker_client,
78+
# NOTE: You can need set DEBUG=true to update the source code
79+
# inside the container. This is useful when you want to test/debug the
80+
# latest code in the runtime docker container.
81+
update_source_code=config.debug,
7782
)
7883
self.container = await self._init_container(
7984
self.sandbox_workspace_dir,
Lines changed: 24 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import os
2-
import shutil
1+
"""
2+
This module contains functions for building and managing the agnostic sandbox image.
3+
4+
This WILL BE DEPRECATED when EventStreamRuntime is fully implemented and adopted.
5+
"""
6+
37
import tempfile
48

59
import docker
610

711
from opendevin.core.logger import opendevin_logger as logger
812

9-
from .source import create_project_source_dist
10-
1113

1214
def generate_dockerfile(base_image: str) -> str:
1315
"""
@@ -36,122 +38,29 @@ def generate_dockerfile(base_image: str) -> str:
3638
return dockerfile_content
3739

3840

39-
def generate_dockerfile_for_eventstream_runtime(
40-
base_image: str, temp_dir: str, skip_init: bool = False
41-
) -> str:
42-
"""
43-
Generate the Dockerfile content for the eventstream runtime image based on user-provided base image.
44-
45-
NOTE: This is only tested on debian yet.
46-
"""
47-
if skip_init:
48-
dockerfile_content = f'FROM {base_image}\n'
49-
else:
50-
dockerfile_content = (
51-
f'FROM {base_image}\n'
52-
# FIXME: make this more generic / cross-platform
53-
'RUN apt update && apt install -y wget sudo\n'
54-
'RUN apt-get update && apt-get install -y libgl1-mesa-glx\n' # Extra dependency for OpenCV
55-
'RUN mkdir -p /opendevin && mkdir -p /opendevin/logs && chmod 777 /opendevin/logs\n'
56-
'RUN echo "" > /opendevin/bash.bashrc\n'
57-
'RUN if [ ! -d /opendevin/miniforge3 ]; then \\\n'
58-
' wget --progress=bar:force -O Miniforge3.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh" && \\\n'
59-
' bash Miniforge3.sh -b -p /opendevin/miniforge3 && \\\n'
60-
' rm Miniforge3.sh && \\\n'
61-
' chmod -R g+w /opendevin/miniforge3 && \\\n'
62-
' bash -c ". /opendevin/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"; \\\n'
63-
' fi\n'
64-
'RUN /opendevin/miniforge3/bin/mamba install python=3.11 -y\n'
65-
'RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry -y\n'
66-
)
67-
68-
tarball_path = create_project_source_dist()
69-
filename = os.path.basename(tarball_path)
70-
filename = filename.removesuffix('.tar.gz')
71-
72-
# move the tarball to temp_dir
73-
_res = shutil.copy(tarball_path, os.path.join(temp_dir, 'project.tar.gz'))
74-
if _res:
75-
os.remove(tarball_path)
76-
logger.info(
77-
f'Source distribution moved to {os.path.join(temp_dir, "project.tar.gz")}'
78-
)
79-
80-
# Copy the project directory to the container
81-
dockerfile_content += 'COPY project.tar.gz /opendevin\n'
82-
# remove /opendevin/code if it exists
83-
dockerfile_content += (
84-
'RUN if [ -d /opendevin/code ]; then rm -rf /opendevin/code; fi\n'
85-
)
86-
# unzip the tarball to /opendevin/code
87-
dockerfile_content += (
88-
'RUN cd /opendevin && tar -xzvf project.tar.gz && rm project.tar.gz\n'
89-
)
90-
dockerfile_content += f'RUN mv /opendevin/{filename} /opendevin/code\n'
91-
# install (or update) the dependencies
92-
dockerfile_content += (
93-
'RUN cd /opendevin/code && '
94-
'/opendevin/miniforge3/bin/mamba run -n base poetry env use python3.11 && '
95-
'/opendevin/miniforge3/bin/mamba run -n base poetry install\n'
96-
# for browser (update if needed)
97-
'RUN apt-get update && cd /opendevin/code && /opendevin/miniforge3/bin/mamba run -n base poetry run playwright install --with-deps chromium\n'
98-
)
99-
return dockerfile_content
100-
101-
10241
def _build_sandbox_image(
103-
base_image: str,
104-
target_image_name: str,
105-
docker_client: docker.DockerClient,
106-
eventstream_runtime: bool = False,
107-
skip_init: bool = False,
42+
base_image: str, target_image_name: str, docker_client: docker.DockerClient
10843
):
10944
try:
11045
with tempfile.TemporaryDirectory() as temp_dir:
111-
if eventstream_runtime:
112-
dockerfile_content = generate_dockerfile_for_eventstream_runtime(
113-
base_image, temp_dir, skip_init=skip_init
114-
)
115-
else:
116-
dockerfile_content = generate_dockerfile(base_image)
46+
dockerfile_content = generate_dockerfile(base_image)
11747

118-
if skip_init:
119-
logger.info(
120-
f'Reusing existing od_sandbox image [{target_image_name}] but will update the source code in it.'
121-
)
122-
logger.info(
123-
(
124-
f'===== Dockerfile content =====\n'
125-
f'{dockerfile_content}\n'
126-
f'==============================='
127-
)
128-
)
129-
else:
130-
logger.info(f'Building agnostic sandbox image: {target_image_name}')
131-
logger.info(
132-
(
133-
f'===== Dockerfile content =====\n'
134-
f'{dockerfile_content}\n'
135-
f'==============================='
136-
)
48+
logger.info(f'Building agnostic sandbox image: {target_image_name}')
49+
logger.info(
50+
(
51+
f'===== Dockerfile content =====\n'
52+
f'{dockerfile_content}\n'
53+
f'==============================='
13754
)
55+
)
13856
with open(f'{temp_dir}/Dockerfile', 'w') as file:
13957
file.write(dockerfile_content)
14058

14159
api_client = docker_client.api
14260
build_logs = api_client.build(
143-
path=temp_dir,
144-
tag=target_image_name,
145-
rm=True,
146-
decode=True,
147-
# do not use cache when skip_init is True (i.e., when we want to update the source code in the existing image)
148-
nocache=skip_init,
61+
path=temp_dir, tag=target_image_name, rm=True, decode=True
14962
)
15063

151-
if skip_init:
152-
logger.info(
153-
f'Rebuilding existing od_sandbox image [{target_image_name}] to update the source code.'
154-
)
15564
for log in build_logs:
15665
if 'stream' in log:
15766
print(log['stream'].strip())
@@ -169,14 +78,8 @@ def _build_sandbox_image(
16978
raise e
17079

17180

172-
def _get_new_image_name(
173-
base_image: str, is_eventstream_runtime: bool, dev_mode: bool = False
174-
) -> str:
81+
def _get_new_image_name(base_image: str) -> str:
17582
prefix = 'od_sandbox'
176-
if is_eventstream_runtime:
177-
prefix = 'od_eventstream_runtime'
178-
if dev_mode:
179-
prefix += '_dev'
18083
if ':' not in base_image:
18184
base_image = base_image + ':latest'
18285

@@ -185,11 +88,7 @@ def _get_new_image_name(
18588
return f'{prefix}:{repo}__{tag}'
18689

18790

188-
def get_od_sandbox_image(
189-
base_image: str,
190-
docker_client: docker.DockerClient,
191-
is_eventstream_runtime: bool = False,
192-
) -> str:
91+
def get_od_sandbox_image(base_image: str, docker_client: docker.DockerClient) -> str:
19392
"""Return the sandbox image name based on user-provided base image.
19493
19594
The returned sandbox image is assumed to contains all the required dependencies for OpenDevin.
@@ -199,52 +98,18 @@ def get_od_sandbox_image(
19998
if 'ghcr.io/opendevin/sandbox' in base_image:
20099
return base_image
201100

202-
new_image_name = _get_new_image_name(base_image, is_eventstream_runtime)
101+
new_image_name = _get_new_image_name(base_image)
203102

204103
# Detect if the sandbox image is built
205-
image_exists = False
206104
images = docker_client.images.list()
207105
for image in images:
208106
if new_image_name in image.tags:
209107
logger.info('Found existing od_sandbox image, reuse:' + new_image_name)
210-
image_exists = True
211-
break
212-
213-
skip_init = False
214-
if image_exists:
215-
if is_eventstream_runtime:
216-
# An eventstream runtime image is already built for the base image (with poetry and dev dependencies)
217-
# but it might not contain the latest version of the source code and dependencies.
218-
# So we need to build a new (dev) image with the latest source code and dependencies.
219-
# FIXME: In production, we should just build once (since the source code will not change)
220-
base_image = new_image_name
221-
new_image_name = _get_new_image_name(
222-
base_image, is_eventstream_runtime, dev_mode=True
223-
)
224-
225-
# Delete the existing image named `new_image_name` if any
226-
images = docker_client.images.list()
227-
for image in images:
228-
if new_image_name in image.tags:
229-
docker_client.images.remove(image.id, force=True)
230-
231-
# We will reuse the existing image but will update the source code in it.
232-
skip_init = True
233-
logger.info(
234-
f'Reusing existing od_sandbox image [{base_image}] but will update the source code into [{new_image_name}]'
235-
)
236-
else:
237108
return new_image_name
238-
else:
239-
# If the sandbox image is not found, build it
240-
logger.info(
241-
f'od_sandbox image is not found for {base_image}, will build: {new_image_name}'
242-
)
243-
_build_sandbox_image(
244-
base_image,
245-
new_image_name,
246-
docker_client,
247-
is_eventstream_runtime,
248-
skip_init=skip_init,
109+
110+
# If the sandbox image is not found, build it
111+
logger.info(
112+
f'od_sandbox image is not found for {base_image}, will build: {new_image_name}'
249113
)
114+
_build_sandbox_image(base_image, new_image_name, docker_client)
250115
return new_image_name

0 commit comments

Comments
 (0)