mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Arch: refactor and add unit tests for EventStreamRuntime docker image build (#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>
This commit is contained in:
parent
959d21c48f
commit
9b1f59a56e
@ -33,7 +33,7 @@ from opendevin.runtime.plugins import (
|
||||
)
|
||||
from opendevin.runtime.runtime import Runtime
|
||||
from opendevin.runtime.utils import find_available_tcp_port
|
||||
from opendevin.runtime.utils.image_agnostic import get_od_sandbox_image
|
||||
from opendevin.runtime.utils.runtime_build import build_runtime_image
|
||||
|
||||
|
||||
class EventStreamRuntime(Runtime):
|
||||
@ -72,8 +72,13 @@ class EventStreamRuntime(Runtime):
|
||||
self.action_semaphore = asyncio.Semaphore(1) # Ensure one action at a time
|
||||
|
||||
async def ainit(self):
|
||||
self.container_image = get_od_sandbox_image(
|
||||
self.container_image, self.docker_client, is_eventstream_runtime=True
|
||||
self.container_image = build_runtime_image(
|
||||
self.container_image,
|
||||
self.docker_client,
|
||||
# NOTE: You can need set DEBUG=true to update the source code
|
||||
# inside the container. This is useful when you want to test/debug the
|
||||
# latest code in the runtime docker container.
|
||||
update_source_code=config.debug,
|
||||
)
|
||||
self.container = await self._init_container(
|
||||
self.sandbox_workspace_dir,
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import os
|
||||
import shutil
|
||||
"""
|
||||
This module contains functions for building and managing the agnostic sandbox image.
|
||||
|
||||
This WILL BE DEPRECATED when EventStreamRuntime is fully implemented and adopted.
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
|
||||
import docker
|
||||
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
|
||||
from .source import create_project_source_dist
|
||||
|
||||
|
||||
def generate_dockerfile(base_image: str) -> str:
|
||||
"""
|
||||
@ -36,122 +38,29 @@ def generate_dockerfile(base_image: str) -> str:
|
||||
return dockerfile_content
|
||||
|
||||
|
||||
def generate_dockerfile_for_eventstream_runtime(
|
||||
base_image: str, temp_dir: str, skip_init: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
Generate the Dockerfile content for the eventstream runtime image based on user-provided base image.
|
||||
|
||||
NOTE: This is only tested on debian yet.
|
||||
"""
|
||||
if skip_init:
|
||||
dockerfile_content = f'FROM {base_image}\n'
|
||||
else:
|
||||
dockerfile_content = (
|
||||
f'FROM {base_image}\n'
|
||||
# FIXME: make this more generic / cross-platform
|
||||
'RUN apt update && apt install -y wget sudo\n'
|
||||
'RUN apt-get update && apt-get install -y libgl1-mesa-glx\n' # Extra dependency for OpenCV
|
||||
'RUN mkdir -p /opendevin && mkdir -p /opendevin/logs && chmod 777 /opendevin/logs\n'
|
||||
'RUN echo "" > /opendevin/bash.bashrc\n'
|
||||
'RUN if [ ! -d /opendevin/miniforge3 ]; then \\\n'
|
||||
' wget --progress=bar:force -O Miniforge3.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh" && \\\n'
|
||||
' bash Miniforge3.sh -b -p /opendevin/miniforge3 && \\\n'
|
||||
' rm Miniforge3.sh && \\\n'
|
||||
' chmod -R g+w /opendevin/miniforge3 && \\\n'
|
||||
' bash -c ". /opendevin/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"; \\\n'
|
||||
' fi\n'
|
||||
'RUN /opendevin/miniforge3/bin/mamba install python=3.11 -y\n'
|
||||
'RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry -y\n'
|
||||
)
|
||||
|
||||
tarball_path = create_project_source_dist()
|
||||
filename = os.path.basename(tarball_path)
|
||||
filename = filename.removesuffix('.tar.gz')
|
||||
|
||||
# move the tarball to temp_dir
|
||||
_res = shutil.copy(tarball_path, os.path.join(temp_dir, 'project.tar.gz'))
|
||||
if _res:
|
||||
os.remove(tarball_path)
|
||||
logger.info(
|
||||
f'Source distribution moved to {os.path.join(temp_dir, "project.tar.gz")}'
|
||||
)
|
||||
|
||||
# Copy the project directory to the container
|
||||
dockerfile_content += 'COPY project.tar.gz /opendevin\n'
|
||||
# remove /opendevin/code if it exists
|
||||
dockerfile_content += (
|
||||
'RUN if [ -d /opendevin/code ]; then rm -rf /opendevin/code; fi\n'
|
||||
)
|
||||
# unzip the tarball to /opendevin/code
|
||||
dockerfile_content += (
|
||||
'RUN cd /opendevin && tar -xzvf project.tar.gz && rm project.tar.gz\n'
|
||||
)
|
||||
dockerfile_content += f'RUN mv /opendevin/{filename} /opendevin/code\n'
|
||||
# install (or update) the dependencies
|
||||
dockerfile_content += (
|
||||
'RUN cd /opendevin/code && '
|
||||
'/opendevin/miniforge3/bin/mamba run -n base poetry env use python3.11 && '
|
||||
'/opendevin/miniforge3/bin/mamba run -n base poetry install\n'
|
||||
# for browser (update if needed)
|
||||
'RUN apt-get update && cd /opendevin/code && /opendevin/miniforge3/bin/mamba run -n base poetry run playwright install --with-deps chromium\n'
|
||||
)
|
||||
return dockerfile_content
|
||||
|
||||
|
||||
def _build_sandbox_image(
|
||||
base_image: str,
|
||||
target_image_name: str,
|
||||
docker_client: docker.DockerClient,
|
||||
eventstream_runtime: bool = False,
|
||||
skip_init: bool = False,
|
||||
base_image: str, target_image_name: str, docker_client: docker.DockerClient
|
||||
):
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
if eventstream_runtime:
|
||||
dockerfile_content = generate_dockerfile_for_eventstream_runtime(
|
||||
base_image, temp_dir, skip_init=skip_init
|
||||
)
|
||||
else:
|
||||
dockerfile_content = generate_dockerfile(base_image)
|
||||
dockerfile_content = generate_dockerfile(base_image)
|
||||
|
||||
if skip_init:
|
||||
logger.info(
|
||||
f'Reusing existing od_sandbox image [{target_image_name}] but will update the source code in it.'
|
||||
)
|
||||
logger.info(
|
||||
(
|
||||
f'===== Dockerfile content =====\n'
|
||||
f'{dockerfile_content}\n'
|
||||
f'==============================='
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.info(f'Building agnostic sandbox image: {target_image_name}')
|
||||
logger.info(
|
||||
(
|
||||
f'===== Dockerfile content =====\n'
|
||||
f'{dockerfile_content}\n'
|
||||
f'==============================='
|
||||
)
|
||||
logger.info(f'Building agnostic sandbox image: {target_image_name}')
|
||||
logger.info(
|
||||
(
|
||||
f'===== Dockerfile content =====\n'
|
||||
f'{dockerfile_content}\n'
|
||||
f'==============================='
|
||||
)
|
||||
)
|
||||
with open(f'{temp_dir}/Dockerfile', 'w') as file:
|
||||
file.write(dockerfile_content)
|
||||
|
||||
api_client = docker_client.api
|
||||
build_logs = api_client.build(
|
||||
path=temp_dir,
|
||||
tag=target_image_name,
|
||||
rm=True,
|
||||
decode=True,
|
||||
# do not use cache when skip_init is True (i.e., when we want to update the source code in the existing image)
|
||||
nocache=skip_init,
|
||||
path=temp_dir, tag=target_image_name, rm=True, decode=True
|
||||
)
|
||||
|
||||
if skip_init:
|
||||
logger.info(
|
||||
f'Rebuilding existing od_sandbox image [{target_image_name}] to update the source code.'
|
||||
)
|
||||
for log in build_logs:
|
||||
if 'stream' in log:
|
||||
print(log['stream'].strip())
|
||||
@ -169,14 +78,8 @@ def _build_sandbox_image(
|
||||
raise e
|
||||
|
||||
|
||||
def _get_new_image_name(
|
||||
base_image: str, is_eventstream_runtime: bool, dev_mode: bool = False
|
||||
) -> str:
|
||||
def _get_new_image_name(base_image: str) -> str:
|
||||
prefix = 'od_sandbox'
|
||||
if is_eventstream_runtime:
|
||||
prefix = 'od_eventstream_runtime'
|
||||
if dev_mode:
|
||||
prefix += '_dev'
|
||||
if ':' not in base_image:
|
||||
base_image = base_image + ':latest'
|
||||
|
||||
@ -185,11 +88,7 @@ def _get_new_image_name(
|
||||
return f'{prefix}:{repo}__{tag}'
|
||||
|
||||
|
||||
def get_od_sandbox_image(
|
||||
base_image: str,
|
||||
docker_client: docker.DockerClient,
|
||||
is_eventstream_runtime: bool = False,
|
||||
) -> str:
|
||||
def get_od_sandbox_image(base_image: str, docker_client: docker.DockerClient) -> str:
|
||||
"""Return the sandbox image name based on user-provided base image.
|
||||
|
||||
The returned sandbox image is assumed to contains all the required dependencies for OpenDevin.
|
||||
@ -199,52 +98,18 @@ def get_od_sandbox_image(
|
||||
if 'ghcr.io/opendevin/sandbox' in base_image:
|
||||
return base_image
|
||||
|
||||
new_image_name = _get_new_image_name(base_image, is_eventstream_runtime)
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
|
||||
# Detect if the sandbox image is built
|
||||
image_exists = False
|
||||
images = docker_client.images.list()
|
||||
for image in images:
|
||||
if new_image_name in image.tags:
|
||||
logger.info('Found existing od_sandbox image, reuse:' + new_image_name)
|
||||
image_exists = True
|
||||
break
|
||||
|
||||
skip_init = False
|
||||
if image_exists:
|
||||
if is_eventstream_runtime:
|
||||
# An eventstream runtime image is already built for the base image (with poetry and dev dependencies)
|
||||
# but it might not contain the latest version of the source code and dependencies.
|
||||
# So we need to build a new (dev) image with the latest source code and dependencies.
|
||||
# FIXME: In production, we should just build once (since the source code will not change)
|
||||
base_image = new_image_name
|
||||
new_image_name = _get_new_image_name(
|
||||
base_image, is_eventstream_runtime, dev_mode=True
|
||||
)
|
||||
|
||||
# Delete the existing image named `new_image_name` if any
|
||||
images = docker_client.images.list()
|
||||
for image in images:
|
||||
if new_image_name in image.tags:
|
||||
docker_client.images.remove(image.id, force=True)
|
||||
|
||||
# We will reuse the existing image but will update the source code in it.
|
||||
skip_init = True
|
||||
logger.info(
|
||||
f'Reusing existing od_sandbox image [{base_image}] but will update the source code into [{new_image_name}]'
|
||||
)
|
||||
else:
|
||||
return new_image_name
|
||||
else:
|
||||
# If the sandbox image is not found, build it
|
||||
logger.info(
|
||||
f'od_sandbox image is not found for {base_image}, will build: {new_image_name}'
|
||||
)
|
||||
_build_sandbox_image(
|
||||
base_image,
|
||||
new_image_name,
|
||||
docker_client,
|
||||
is_eventstream_runtime,
|
||||
skip_init=skip_init,
|
||||
|
||||
# If the sandbox image is not found, build it
|
||||
logger.info(
|
||||
f'od_sandbox image is not found for {base_image}, will build: {new_image_name}'
|
||||
)
|
||||
_build_sandbox_image(base_image, new_image_name, docker_client)
|
||||
return new_image_name
|
||||
|
||||
242
opendevin/runtime/utils/runtime_build.py
Normal file
242
opendevin/runtime/utils/runtime_build.py
Normal file
@ -0,0 +1,242 @@
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from importlib.metadata import version
|
||||
|
||||
import docker
|
||||
|
||||
import opendevin
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
|
||||
|
||||
def _create_project_source_dist():
|
||||
"""Create a source distribution of the project. Return the path to the tarball."""
|
||||
|
||||
# Copy the project directory to the container
|
||||
# get the location of "opendevin" package
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(opendevin.__file__)))
|
||||
logger.info(f'Using project root: {project_root}')
|
||||
|
||||
# run "python -m build -s" on project_root
|
||||
result = subprocess.run(['python', '-m', 'build', '-s', project_root])
|
||||
if result.returncode != 0:
|
||||
logger.error(f'Build failed: {result}')
|
||||
raise Exception(f'Build failed: {result}')
|
||||
|
||||
tarball_path = os.path.join(
|
||||
project_root, 'dist', f'opendevin-{version("opendevin")}.tar.gz'
|
||||
)
|
||||
if not os.path.exists(tarball_path):
|
||||
logger.error(f'Source distribution not found at {tarball_path}')
|
||||
raise Exception(f'Source distribution not found at {tarball_path}')
|
||||
logger.info(f'Source distribution created at {tarball_path}')
|
||||
|
||||
return tarball_path
|
||||
|
||||
|
||||
def _put_source_code_to_dir(temp_dir: str) -> str:
|
||||
tarball_path = _create_project_source_dist()
|
||||
filename = os.path.basename(tarball_path)
|
||||
filename = filename.removesuffix('.tar.gz')
|
||||
|
||||
# move the tarball to temp_dir
|
||||
_res = shutil.copy(tarball_path, os.path.join(temp_dir, 'project.tar.gz'))
|
||||
if _res:
|
||||
os.remove(tarball_path)
|
||||
logger.info(
|
||||
f'Source distribution moved to {os.path.join(temp_dir, "project.tar.gz")}'
|
||||
)
|
||||
return filename
|
||||
|
||||
|
||||
def _generate_dockerfile(
|
||||
base_image: str, source_code_dirname: str, skip_init: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
Generate the Dockerfile content for the eventstream runtime image based on user-provided base image.
|
||||
|
||||
NOTE: This is only tested on debian yet.
|
||||
"""
|
||||
if skip_init:
|
||||
dockerfile_content = f'FROM {base_image}\n'
|
||||
else:
|
||||
dockerfile_content = (
|
||||
f'FROM {base_image}\n'
|
||||
# FIXME: make this more generic / cross-platform
|
||||
'RUN apt update && apt install -y wget sudo\n'
|
||||
'RUN apt-get update && apt-get install -y libgl1-mesa-glx\n' # Extra dependency for OpenCV
|
||||
'RUN mkdir -p /opendevin && mkdir -p /opendevin/logs && chmod 777 /opendevin/logs\n'
|
||||
'RUN echo "" > /opendevin/bash.bashrc\n'
|
||||
'RUN if [ ! -d /opendevin/miniforge3 ]; then \\\n'
|
||||
' wget --progress=bar:force -O Miniforge3.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh" && \\\n'
|
||||
' bash Miniforge3.sh -b -p /opendevin/miniforge3 && \\\n'
|
||||
' rm Miniforge3.sh && \\\n'
|
||||
' chmod -R g+w /opendevin/miniforge3 && \\\n'
|
||||
' bash -c ". /opendevin/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"; \\\n'
|
||||
' fi\n'
|
||||
'RUN /opendevin/miniforge3/bin/mamba install python=3.11 -y\n'
|
||||
'RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry -y\n'
|
||||
)
|
||||
|
||||
# Copy the project directory to the container
|
||||
dockerfile_content += 'COPY project.tar.gz /opendevin\n'
|
||||
# remove /opendevin/code if it exists
|
||||
dockerfile_content += (
|
||||
'RUN if [ -d /opendevin/code ]; then rm -rf /opendevin/code; fi\n'
|
||||
)
|
||||
# unzip the tarball to /opendevin/code
|
||||
dockerfile_content += (
|
||||
'RUN cd /opendevin && tar -xzvf project.tar.gz && rm project.tar.gz\n'
|
||||
)
|
||||
dockerfile_content += f'RUN mv /opendevin/{source_code_dirname} /opendevin/code\n'
|
||||
# install (or update) the dependencies
|
||||
dockerfile_content += (
|
||||
'RUN cd /opendevin/code && '
|
||||
'/opendevin/miniforge3/bin/mamba run -n base poetry env use python3.11 && '
|
||||
'/opendevin/miniforge3/bin/mamba run -n base poetry install\n'
|
||||
# for browser (update if needed)
|
||||
'RUN apt-get update && cd /opendevin/code && /opendevin/miniforge3/bin/mamba run -n base poetry run playwright install --with-deps chromium\n'
|
||||
)
|
||||
return dockerfile_content
|
||||
|
||||
|
||||
def _build_sandbox_image(
|
||||
base_image: str,
|
||||
target_image_name: str,
|
||||
docker_client: docker.DockerClient,
|
||||
skip_init: bool = False,
|
||||
):
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
source_code_dirname = _put_source_code_to_dir(temp_dir)
|
||||
dockerfile_content = _generate_dockerfile(
|
||||
base_image, source_code_dirname, skip_init=skip_init
|
||||
)
|
||||
if skip_init:
|
||||
logger.info(
|
||||
f'Reusing existing od_sandbox image [{target_image_name}] but will update the source code in it.'
|
||||
)
|
||||
else:
|
||||
logger.info(f'Building agnostic sandbox image: {target_image_name}')
|
||||
logger.info(
|
||||
(
|
||||
f'===== Dockerfile content =====\n'
|
||||
f'{dockerfile_content}\n'
|
||||
f'==============================='
|
||||
)
|
||||
)
|
||||
with open(f'{temp_dir}/Dockerfile', 'w') as file:
|
||||
file.write(dockerfile_content)
|
||||
|
||||
api_client = docker_client.api
|
||||
build_logs = api_client.build(
|
||||
path=temp_dir,
|
||||
tag=target_image_name,
|
||||
rm=True,
|
||||
decode=True,
|
||||
# do not use cache when skip_init is True (i.e., when we want to update the source code in the existing image)
|
||||
nocache=skip_init,
|
||||
)
|
||||
|
||||
if skip_init:
|
||||
logger.info(
|
||||
f'Rebuilding existing od_sandbox image [{target_image_name}] to update the source code.'
|
||||
)
|
||||
for log in build_logs:
|
||||
if 'stream' in log:
|
||||
print(log['stream'].strip())
|
||||
elif 'error' in log:
|
||||
logger.error(log['error'].strip())
|
||||
else:
|
||||
logger.info(str(log))
|
||||
|
||||
logger.info(f'Image {target_image_name} built successfully')
|
||||
except docker.errors.BuildError as e:
|
||||
logger.error(f'Sandbox image build failed: {e}')
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f'An error occurred during sandbox image build: {e}')
|
||||
raise e
|
||||
|
||||
|
||||
def _get_new_image_name(base_image: str, dev_mode: bool = False) -> str:
|
||||
if dev_mode:
|
||||
if 'od_runtime' not in base_image:
|
||||
raise ValueError(
|
||||
f'Base image {base_image} must be a valid od_runtime image to be used for dev mode.'
|
||||
)
|
||||
# remove the 'od_runtime' prefix from the base_image
|
||||
return base_image.replace('od_runtime', 'od_runtime_dev')
|
||||
else:
|
||||
prefix = 'od_runtime'
|
||||
if ':' not in base_image:
|
||||
base_image = base_image + ':latest'
|
||||
[repo, tag] = base_image.split(':')
|
||||
repo = repo.replace('/', '___')
|
||||
return f'{prefix}:{repo}_tag_{tag}'
|
||||
|
||||
|
||||
def _check_image_exists(image_name: str, docker_client: docker.DockerClient) -> bool:
|
||||
images = docker_client.images.list()
|
||||
for image in images:
|
||||
if image_name in image.tags:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def build_runtime_image(
|
||||
base_image: str,
|
||||
docker_client: docker.DockerClient,
|
||||
update_source_code: bool = False,
|
||||
) -> str:
|
||||
"""Build the runtime image for the OpenDevin runtime.
|
||||
|
||||
This is only used for **eventstream runtime**.
|
||||
"""
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
|
||||
# Try to pull the new image from the registry
|
||||
try:
|
||||
docker_client.images.pull(new_image_name)
|
||||
except docker.errors.ImageNotFound:
|
||||
logger.info(f'Image {new_image_name} not found, building it from scratch')
|
||||
|
||||
# Detect if the sandbox image is built
|
||||
image_exists = _check_image_exists(new_image_name, docker_client)
|
||||
|
||||
skip_init = False
|
||||
if image_exists and not update_source_code:
|
||||
# If (1) Image exists & we are not updating the source code, we can reuse the existing production image
|
||||
return new_image_name
|
||||
elif image_exists and update_source_code:
|
||||
# If (2) Image exists & we plan to update the source code (in dev mode), we need to rebuild the image
|
||||
# and give it a special name
|
||||
# e.g., od_runtime:ubuntu_tag_latest -> od_runtime_dev:ubuntu_tag_latest
|
||||
base_image = new_image_name
|
||||
new_image_name = _get_new_image_name(base_image, dev_mode=True)
|
||||
|
||||
skip_init = True # since we only need to update the source code
|
||||
else:
|
||||
# If (3) Image does not exist, we need to build it from scratch
|
||||
# e.g., ubuntu:latest -> od_runtime:ubuntu_tag_latest
|
||||
skip_init = False # since we need to build the image from scratch
|
||||
|
||||
logger.info(f'Building image [{new_image_name}] from scratch')
|
||||
|
||||
_build_sandbox_image(base_image, new_image_name, docker_client, skip_init=skip_init)
|
||||
return new_image_name
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--base_image', type=str, default='ubuntu:latest')
|
||||
parser.add_argument('--update_source_code', type=bool, default=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
client = docker.from_env()
|
||||
image_name = build_runtime_image(
|
||||
args.base_image, client, update_source_code=args.update_source_code
|
||||
)
|
||||
print(f'\nBUILT Image: {image_name}\n')
|
||||
@ -1,32 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
from importlib.metadata import version
|
||||
|
||||
import opendevin
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
|
||||
|
||||
def create_project_source_dist():
|
||||
"""Create a source distribution of the project. Return the path to the tarball."""
|
||||
|
||||
# Copy the project directory to the container
|
||||
# get the location of "opendevin" package
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(opendevin.__file__)))
|
||||
logger.info(f'Using project root: {project_root}')
|
||||
|
||||
# run "python -m build -s" on project_root
|
||||
result = subprocess.run(['python', '-m', 'build', '-s', project_root])
|
||||
if result.returncode != 0:
|
||||
logger.error(f'Build failed: {result}')
|
||||
raise Exception(f'Build failed: {result}')
|
||||
logger.info(f'Source distribution create result: {result}')
|
||||
|
||||
tarball_path = os.path.join(
|
||||
project_root, 'dist', f'opendevin-{version("opendevin")}.tar.gz'
|
||||
)
|
||||
if not os.path.exists(tarball_path):
|
||||
logger.error(f'Source distribution not found at {tarball_path}')
|
||||
raise Exception(f'Source distribution not found at {tarball_path}')
|
||||
logger.info(f'Source distribution created at {tarball_path}')
|
||||
|
||||
return tarball_path
|
||||
@ -20,15 +20,15 @@ def test_generate_dockerfile():
|
||||
def test_get_new_image_name_legacy():
|
||||
# test non-eventstream runtime (sandbox-based)
|
||||
base_image = 'debian:11'
|
||||
new_image_name = _get_new_image_name(base_image, is_eventstream_runtime=False)
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
assert new_image_name == 'od_sandbox:debian__11'
|
||||
|
||||
base_image = 'ubuntu:22.04'
|
||||
new_image_name = _get_new_image_name(base_image, is_eventstream_runtime=False)
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
assert new_image_name == 'od_sandbox:ubuntu__22.04'
|
||||
|
||||
base_image = 'ubuntu'
|
||||
new_image_name = _get_new_image_name(base_image, is_eventstream_runtime=False)
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
assert new_image_name == 'od_sandbox:ubuntu__latest'
|
||||
|
||||
|
||||
@ -47,11 +47,5 @@ def test_get_od_sandbox_image(mock_docker_client, mock_build_sandbox_image):
|
||||
image_name = get_od_sandbox_image(base_image, mock_docker_client)
|
||||
assert image_name == 'od_sandbox:debian__11'
|
||||
mock_build_sandbox_image.assert_called_once_with(
|
||||
base_image,
|
||||
'od_sandbox:debian__11',
|
||||
mock_docker_client,
|
||||
# eventstream runtime specific arguments, not used for sandbox-based runtime
|
||||
# is_eventstream_runtime=
|
||||
False,
|
||||
skip_init=False,
|
||||
base_image, 'od_sandbox:debian__11', mock_docker_client
|
||||
)
|
||||
|
||||
191
tests/unit/test_runtime_build.py
Normal file
191
tests/unit/test_runtime_build.py
Normal file
@ -0,0 +1,191 @@
|
||||
import os
|
||||
import tarfile
|
||||
import tempfile
|
||||
from importlib.metadata import version
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import toml
|
||||
|
||||
from opendevin.runtime.utils.runtime_build import (
|
||||
_generate_dockerfile,
|
||||
_get_new_image_name,
|
||||
_put_source_code_to_dir,
|
||||
build_runtime_image,
|
||||
)
|
||||
|
||||
RUNTIME_IMAGE_PREFIX = 'od_runtime'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir():
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
yield temp_dir
|
||||
|
||||
|
||||
def test_put_source_code_to_dir(temp_dir):
|
||||
folder_name = _put_source_code_to_dir(temp_dir)
|
||||
|
||||
# assert there is a file called 'project.tar.gz' in the temp_dir
|
||||
assert os.path.exists(os.path.join(temp_dir, 'project.tar.gz'))
|
||||
|
||||
# untar the file
|
||||
with tarfile.open(os.path.join(temp_dir, 'project.tar.gz'), 'r:gz') as tar:
|
||||
tar.extractall(path=temp_dir)
|
||||
|
||||
# check the source file is the same as the current code base
|
||||
assert os.path.exists(os.path.join(temp_dir, folder_name, 'pyproject.toml'))
|
||||
# make sure the version from the pyproject.toml is the same as the current version
|
||||
with open(os.path.join(temp_dir, folder_name, 'pyproject.toml'), 'r') as f:
|
||||
pyproject = toml.load(f)
|
||||
_pyproject_version = pyproject['tool']['poetry']['version']
|
||||
assert _pyproject_version == version('opendevin')
|
||||
|
||||
|
||||
def test_generate_dockerfile_scratch():
|
||||
base_image = 'debian:11'
|
||||
source_code_dirname = 'dummy'
|
||||
dockerfile_content = _generate_dockerfile(
|
||||
base_image,
|
||||
source_code_dirname=source_code_dirname,
|
||||
skip_init=False,
|
||||
)
|
||||
assert base_image in dockerfile_content
|
||||
assert 'RUN apt update && apt install -y wget sudo' in dockerfile_content
|
||||
assert (
|
||||
'RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry -y'
|
||||
in dockerfile_content
|
||||
)
|
||||
|
||||
# Check the update command
|
||||
assert (
|
||||
f'RUN mv /opendevin/{source_code_dirname} /opendevin/code' in dockerfile_content
|
||||
)
|
||||
assert (
|
||||
'/opendevin/miniforge3/bin/mamba run -n base poetry install'
|
||||
in dockerfile_content
|
||||
)
|
||||
|
||||
|
||||
def test_generate_dockerfile_skip_init():
|
||||
base_image = 'debian:11'
|
||||
source_code_dirname = 'dummy'
|
||||
dockerfile_content = _generate_dockerfile(
|
||||
base_image,
|
||||
source_code_dirname=source_code_dirname,
|
||||
skip_init=True,
|
||||
)
|
||||
|
||||
# These commands SHOULD NOT include in the dockerfile if skip_init is True
|
||||
assert 'RUN apt update && apt install -y wget sudo' not in dockerfile_content
|
||||
assert (
|
||||
'RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry -y'
|
||||
not in dockerfile_content
|
||||
)
|
||||
|
||||
# These update commands SHOULD still in the dockerfile
|
||||
assert (
|
||||
f'RUN mv /opendevin/{source_code_dirname} /opendevin/code' in dockerfile_content
|
||||
)
|
||||
assert (
|
||||
'/opendevin/miniforge3/bin/mamba run -n base poetry install'
|
||||
in dockerfile_content
|
||||
)
|
||||
|
||||
|
||||
def test_get_new_image_name_eventstream():
|
||||
base_image = 'debian:11'
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
assert new_image_name == f'{RUNTIME_IMAGE_PREFIX}:debian_tag_11'
|
||||
|
||||
base_image = 'ubuntu:22.04'
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
assert new_image_name == f'{RUNTIME_IMAGE_PREFIX}:ubuntu_tag_22.04'
|
||||
|
||||
base_image = 'ubuntu'
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
assert new_image_name == f'{RUNTIME_IMAGE_PREFIX}:ubuntu_tag_latest'
|
||||
|
||||
|
||||
def test_get_new_image_name_eventstream_dev_mode():
|
||||
base_image = f'{RUNTIME_IMAGE_PREFIX}:debian_tag_11'
|
||||
new_image_name = _get_new_image_name(base_image, dev_mode=True)
|
||||
assert new_image_name == f'{RUNTIME_IMAGE_PREFIX}_dev:debian_tag_11'
|
||||
|
||||
base_image = f'{RUNTIME_IMAGE_PREFIX}:ubuntu_tag_22.04'
|
||||
new_image_name = _get_new_image_name(base_image, dev_mode=True)
|
||||
assert new_image_name == f'{RUNTIME_IMAGE_PREFIX}_dev:ubuntu_tag_22.04'
|
||||
|
||||
base_image = f'{RUNTIME_IMAGE_PREFIX}:ubuntu_tag_latest'
|
||||
new_image_name = _get_new_image_name(base_image, dev_mode=True)
|
||||
assert new_image_name == f'{RUNTIME_IMAGE_PREFIX}_dev:ubuntu_tag_latest'
|
||||
|
||||
|
||||
def test_get_new_image_name_eventstream_dev_invalid_base_image():
|
||||
with pytest.raises(ValueError):
|
||||
base_image = 'debian:11'
|
||||
_get_new_image_name(base_image, dev_mode=True)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
base_image = 'ubuntu:22.04'
|
||||
_get_new_image_name(base_image, dev_mode=True)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
base_image = 'ubuntu:latest'
|
||||
_get_new_image_name(base_image, dev_mode=True)
|
||||
|
||||
|
||||
@patch('opendevin.runtime.utils.runtime_build._build_sandbox_image')
|
||||
@patch('opendevin.runtime.utils.runtime_build.docker.DockerClient')
|
||||
def test_build_runtime_image_from_scratch(mock_docker_client, mock_build_sandbox_image):
|
||||
base_image = 'debian:11'
|
||||
mock_docker_client.images.list.return_value = []
|
||||
|
||||
image_name = build_runtime_image(base_image, mock_docker_client)
|
||||
assert image_name == f'{RUNTIME_IMAGE_PREFIX}:debian_tag_11'
|
||||
|
||||
mock_build_sandbox_image.assert_called_once_with(
|
||||
base_image,
|
||||
f'{RUNTIME_IMAGE_PREFIX}:debian_tag_11',
|
||||
mock_docker_client,
|
||||
skip_init=False,
|
||||
)
|
||||
|
||||
|
||||
@patch('opendevin.runtime.utils.runtime_build._build_sandbox_image')
|
||||
@patch('opendevin.runtime.utils.runtime_build.docker.DockerClient')
|
||||
def test_build_runtime_image_exist_no_update_source(
|
||||
mock_docker_client, mock_build_sandbox_image
|
||||
):
|
||||
base_image = 'debian:11'
|
||||
mock_docker_client.images.list.return_value = [
|
||||
MagicMock(tags=[f'{RUNTIME_IMAGE_PREFIX}:debian_tag_11'])
|
||||
]
|
||||
|
||||
image_name = build_runtime_image(base_image, mock_docker_client)
|
||||
assert image_name == f'{RUNTIME_IMAGE_PREFIX}:debian_tag_11'
|
||||
|
||||
mock_build_sandbox_image.assert_not_called()
|
||||
|
||||
|
||||
@patch('opendevin.runtime.utils.runtime_build._build_sandbox_image')
|
||||
@patch('opendevin.runtime.utils.runtime_build.docker.DockerClient')
|
||||
def test_build_runtime_image_exist_with_update_source(
|
||||
mock_docker_client, mock_build_sandbox_image
|
||||
):
|
||||
base_image = 'debian:11'
|
||||
mock_docker_client.images.list.return_value = [
|
||||
MagicMock(tags=[f'{RUNTIME_IMAGE_PREFIX}:debian_tag_11'])
|
||||
]
|
||||
|
||||
image_name = build_runtime_image(
|
||||
base_image, mock_docker_client, update_source_code=True
|
||||
)
|
||||
assert image_name == f'{RUNTIME_IMAGE_PREFIX}_dev:debian_tag_11'
|
||||
|
||||
mock_build_sandbox_image.assert_called_once_with(
|
||||
f'{RUNTIME_IMAGE_PREFIX}:debian_tag_11',
|
||||
f'{RUNTIME_IMAGE_PREFIX}_dev:debian_tag_11',
|
||||
mock_docker_client,
|
||||
skip_init=True,
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user