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:
Xingyao Wang 2024-07-15 09:27:31 +08:00 committed by GitHub
parent 959d21c48f
commit 9b1f59a56e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 469 additions and 204 deletions

View File

@ -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,

View File

@ -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

View 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')

View File

@ -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

View File

@ -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
)

View 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,
)