fix: pip not available in runtime (#3306)

* try to fix pip unavailable

* update test case for pip

* force rebuild in CI

* remove extra symlink

* fix newline

* added semi-colon to line 31

* Dockerfile.j2: activate env at the end

* Revert "Dockerfile.j2: activate env at the end"

This reverts commit cf2f565102.

* cleanup Dockerfile

* switch default python image

* remove image agnostic (no longer used)

* fix tests

* switch to nikolaik/python-nodejs:python3.11-nodejs22

* fix test

* fix test

* revert docker

* update template

---------

Co-authored-by: tobitege <tobitege@gmx.de>
Co-authored-by: Graham Neubig <neubig@gmail.com>
This commit is contained in:
Xingyao Wang
2024-08-10 03:04:43 +08:00
committed by GitHub
parent 00bc68642f
commit bdf6df12c3
21 changed files with 50 additions and 201 deletions

View File

@@ -81,7 +81,7 @@ jobs:
strategy:
matrix:
image: ['od_runtime']
base_image: ['ubuntu:22.04']
base_image: ['nikolaik/python-nodejs:python3.11-nodejs22']
platform: ['amd64', 'arm64']
steps:
- name: Checkout
@@ -115,7 +115,7 @@ jobs:
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
- name: Create source distribution and Dockerfile
run: poetry run python3 opendevin/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image }} --build_folder containers/runtime
run: poetry run python3 opendevin/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image }} --build_folder containers/runtime --force_rebuild
- name: Build and export image
id: build
run: |

View File

@@ -174,7 +174,7 @@ llm_config = 'gpt3'
#user_id = 1000
# Container image to use for the sandbox
#container_image = "ghcr.io/opendevin/sandbox:main"
#container_image = "nikolaik/python-nodejs:python3.11-nodejs22"
# Use host network
#use_host_network = false

View File

@@ -62,7 +62,7 @@ def get_config(
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
container_image='ubuntu:22.04',
container_image='python:3.11-bookworm',
enable_auto_lint=False,
use_host_network=False,
),

View File

@@ -44,7 +44,7 @@ def get_config(
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
container_image='ubuntu:22.04',
container_image='python:3.11-bookworm',
enable_auto_lint=True,
use_host_network=False,
),

View File

@@ -75,7 +75,7 @@ def get_config(
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
container_image='ubuntu:22.04',
container_image='python:3.11-bookworm',
enable_auto_lint=True,
use_host_network=False,
),

View File

@@ -40,7 +40,7 @@ def get_config(
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
container_image='ubuntu:22.04',
container_image='python:3.11-bookworm',
enable_auto_lint=False,
use_host_network=False,
),

View File

@@ -51,7 +51,7 @@ def get_config(
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
container_image='ubuntu:22.04',
container_image='python:3.11-bookworm',
enable_auto_lint=True,
use_host_network=False,
),

View File

@@ -42,7 +42,7 @@ def get_config(
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
container_image='ubuntu:22.04',
container_image='python:3.11-bookworm',
enable_auto_lint=True,
use_host_network=False,
),

View File

@@ -65,7 +65,7 @@ def get_config(
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
container_image='ubuntu:22.04',
container_image='python:3.11-bookworm',
enable_auto_lint=True,
use_host_network=False,
),

View File

@@ -86,7 +86,7 @@ def get_config(
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
container_image='ubuntu:22.04',
container_image='python:3.11-bookworm',
enable_auto_lint=True,
use_host_network=False,
),

View File

@@ -45,7 +45,7 @@ def get_config(
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
container_image='ubuntu:22.04',
container_image='python:3.11-bookworm',
enable_auto_lint=True,
use_host_network=False,
),

View File

@@ -54,7 +54,7 @@ def get_config(
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
container_image='ubuntu:22.04',
container_image='python:3.11-bookworm',
enable_auto_lint=True,
use_host_network=False,
browsergym_eval_env=env_id,

View File

@@ -166,9 +166,7 @@ class SandboxConfig(metaclass=Singleton):
"""
api_hostname: str = 'localhost'
container_image: str = (
'ubuntu:22.04' # default to ubuntu:22.04 for eventstream runtime
)
container_image: str = 'nikolaik/python-nodejs:python3.11-nodejs22' # default to nikolaik/python-nodejs:python3.11-nodejs22 for eventstream runtime
user_id: int = os.getuid() if hasattr(os, 'getuid') else 1000
timeout: int = 120
enable_auto_lint: bool = (

View File

@@ -1,113 +0,0 @@
"""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
def generate_dockerfile(base_image: str) -> str:
"""Generate the Dockerfile content for the agnostic sandbox image based on user-provided base image.
NOTE: This is only tested on debian yet.
"""
# FIXME: Remove the requirement of ssh in future version
dockerfile_content = (
f'FROM {base_image}\n'
'RUN apt update && apt install -y openssh-server wget sudo\n'
'RUN mkdir -p -m0755 /var/run/sshd\n'
'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/pip install --upgrade pip\n'
'RUN /opendevin/miniforge3/bin/pip install jupyterlab notebook jupyter_kernel_gateway flake8\n'
'RUN /opendevin/miniforge3/bin/pip install python-docx PyPDF2 python-pptx pylatexenc openai\n'
).strip()
return dockerfile_content
def _build_sandbox_image(
base_image: str, target_image_name: str, docker_client: docker.DockerClient
):
try:
with tempfile.TemporaryDirectory() as temp_dir:
dockerfile_content = generate_dockerfile(base_image)
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
)
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) -> str:
prefix = 'od_sandbox'
if ':' not in base_image:
base_image = base_image + ':latest'
[repo, tag] = base_image.split(':')
repo = repo.replace('/', '___')
return f'{prefix}:{repo}__{tag}'
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.
If the sandbox image is not found, it will be built.
"""
# OpenDevin's offcial sandbox already contains the required dependencies for OpenDevin.
if 'ghcr.io/opendevin/sandbox' in base_image:
return base_image
new_image_name = _get_new_image_name(base_image)
# Detect if the sandbox image is built
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)
return new_image_name
# 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

@@ -239,6 +239,7 @@ def build_runtime_image(
extra_deps: str | None = None,
docker_build_folder: str | None = None,
dry_run: bool = False,
force_rebuild: bool = False,
) -> str:
"""Build the runtime image for the OpenDevin runtime.
@@ -277,7 +278,10 @@ def build_runtime_image(
# 2. If the exact hash is not found, we will FIRST try to re-build it
# by leveraging the non-hash `generic_runtime_image_name` to save some time
# from re-building the dependencies (e.g., poetry install, apt install)
elif _check_image_exists(generic_runtime_image_name, docker_client):
elif (
_check_image_exists(generic_runtime_image_name, docker_client)
and not force_rebuild
):
logger.info(
f'Cannot find matched hash for image [{hash_runtime_image_name}]\n'
f'Will try to re-build it from latest [{generic_runtime_image_name}] image to potentially save '
@@ -319,6 +323,10 @@ def build_runtime_image(
# 3. If the image is not found AND we cannot re-use the non-hash latest relavant image,
# we will build it completely from scratch
else:
if force_rebuild:
logger.info(
f'Force re-build: Will try to re-build image [{generic_runtime_image_name}] from scratch.\n'
)
cur_docker_build_folder = docker_build_folder or tempfile.mkdtemp()
_new_from_scratch_hash = prep_docker_build_folder(
cur_docker_build_folder,
@@ -352,8 +360,11 @@ def build_runtime_image(
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--base_image', type=str, default='ubuntu:22.04')
parser.add_argument(
'--base_image', type=str, default='nikolaik/python-nodejs:python3.11-nodejs22'
)
parser.add_argument('--build_folder', type=str, default=None)
parser.add_argument('--force_rebuild', action='store_true', default=False)
args = parser.parse_args()
if args.build_folder is not None:
@@ -373,6 +384,7 @@ if __name__ == '__main__':
docker_client=docker.from_env(),
docker_build_folder=temp_dir,
dry_run=True,
force_rebuild=args.force_rebuild,
)
_runtime_image_repo, runtime_image_hash_tag = runtime_image_hash_name.split(
':'

View File

@@ -18,13 +18,6 @@ RUN apt-get update && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install Python if not already installed
RUN if [ ! -e /usr/bin/python ]; then \
apt-get update && \
apt-get install -y python3 && \
ln -s /usr/bin/python3 /usr/bin/python; \
fi
# Create necessary directories
RUN mkdir -p /opendevin && \
mkdir -p /opendevin/logs && \

View File

@@ -59,7 +59,7 @@ mkdir -p $WORKSPACE_BASE
TEST_RUNTIME="${TEST_RUNTIME:-eventstream}" # can be server or eventstream
# TODO: set this as default after ServerRuntime is deprecated
if [ "$TEST_RUNTIME" == "eventstream" ] && [ -z "$SANDBOX_CONTAINER_IMAGE" ]; then
SANDBOX_CONTAINER_IMAGE="ubuntu:22.04"
SANDBOX_CONTAINER_IMAGE="nikolaik/python-nodejs:python3.11-nodejs22"
fi
MAX_ITERATIONS=15

View File

@@ -358,7 +358,7 @@ def test_defaults_dict_after_updates(default_config):
assert defaults_after_updates['sandbox']['timeout']['default'] == 120
assert (
defaults_after_updates['sandbox']['container_image']['default']
== 'ubuntu:22.04'
== 'nikolaik/python-nodejs:python3.11-nodejs22'
)
assert defaults_after_updates == initial_defaults

View File

@@ -1,51 +0,0 @@
from unittest.mock import MagicMock, patch
from opendevin.runtime.utils.image_agnostic import (
_get_new_image_name,
generate_dockerfile,
get_od_sandbox_image,
)
def test_generate_dockerfile():
base_image = 'debian:11'
dockerfile_content = generate_dockerfile(base_image)
assert base_image in dockerfile_content
assert (
'RUN apt update && apt install -y openssh-server wget sudo'
in dockerfile_content
)
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)
assert new_image_name == 'od_sandbox:debian__11'
base_image = 'ubuntu:22.04'
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)
assert new_image_name == 'od_sandbox:ubuntu__latest'
@patch('opendevin.runtime.utils.image_agnostic._build_sandbox_image')
@patch('opendevin.runtime.utils.image_agnostic.docker.DockerClient')
def test_get_od_sandbox_image(mock_docker_client, mock_build_sandbox_image):
base_image = 'debian:11'
mock_docker_client.images.list.return_value = [
MagicMock(tags=['od_sandbox:debian__11'])
]
image_name = get_od_sandbox_image(base_image, mock_docker_client)
assert image_name == 'od_sandbox:debian__11'
mock_docker_client.images.list.return_value = []
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
)

View File

@@ -83,7 +83,9 @@ def enable_auto_lint(request):
return request.param
@pytest.fixture(scope='module', params=['ubuntu:22.04', 'debian:11'])
@pytest.fixture(
scope='module', params=['nikolaik/python-nodejs:python3.11-nodejs22', 'debian:11']
)
def container_image(request):
time.sleep(1)
return request.param
@@ -127,7 +129,7 @@ async def _load_runtime(
if 'od_runtime' not in cur_container_image and cur_container_image not in {
'xingyaoww/od-eval-miniwob:v1.0'
}: # a special exception list
cur_container_image = 'ubuntu:22.04'
cur_container_image = 'nikolaik/python-nodejs:python3.11-nodejs22'
logger.warning(
f'`{config.sandbox.container_image}` is not an od_runtime image. Will use `{cur_container_image}` as the container image for testing.'
)
@@ -1033,6 +1035,13 @@ async def test_bash_python_version(temp_dir, box_class):
assert obs.exit_code == 0
# Should not error out
action = CmdRunAction(command='pip --version')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = await runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
# Should not error out
await runtime.close()
await asyncio.sleep(1)

View File

@@ -62,7 +62,7 @@ def test_put_source_code_to_dir(temp_dir):
def test_docker_build_folder(temp_dir):
prep_docker_build_folder(
temp_dir,
base_image='ubuntu:22.04',
base_image='nikolaik/python-nodejs:python3.11-nodejs22',
skip_init=False,
)
@@ -81,14 +81,14 @@ def test_docker_build_folder(temp_dir):
def test_hash_folder_same(temp_dir):
dir_hash_1 = prep_docker_build_folder(
temp_dir,
base_image='ubuntu:22.04',
base_image='nikolaik/python-nodejs:python3.11-nodejs22',
skip_init=False,
)
with tempfile.TemporaryDirectory() as temp_dir_2:
dir_hash_2 = prep_docker_build_folder(
temp_dir_2,
base_image='ubuntu:22.04',
base_image='nikolaik/python-nodejs:python3.11-nodejs22',
skip_init=False,
)
assert dir_hash_1 == dir_hash_2
@@ -97,14 +97,14 @@ def test_hash_folder_same(temp_dir):
def test_hash_folder_diff_init(temp_dir):
dir_hash_1 = prep_docker_build_folder(
temp_dir,
base_image='ubuntu:22.04',
base_image='nikolaik/python-nodejs:python3.11-nodejs22',
skip_init=False,
)
with tempfile.TemporaryDirectory() as temp_dir_2:
dir_hash_2 = prep_docker_build_folder(
temp_dir_2,
base_image='ubuntu:22.04',
base_image='nikolaik/python-nodejs:python3.11-nodejs22',
skip_init=True,
)
assert dir_hash_1 != dir_hash_2
@@ -113,7 +113,7 @@ def test_hash_folder_diff_init(temp_dir):
def test_hash_folder_diff_image(temp_dir):
dir_hash_1 = prep_docker_build_folder(
temp_dir,
base_image='ubuntu:22.04',
base_image='nikolaik/python-nodejs:python3.11-nodejs22',
skip_init=False,
)
@@ -178,11 +178,12 @@ def test_get_runtime_image_repo_and_tag_eventstream():
and img_tag == f'{OD_VERSION}_image_debian_tag_11'
)
base_image = 'ubuntu:22.04'
base_image = 'nikolaik/python-nodejs:python3.11-nodejs22'
img_repo, img_tag = get_runtime_image_repo_and_tag(base_image)
assert (
img_repo == f'{RUNTIME_IMAGE_REPO}'
and img_tag == f'{OD_VERSION}_image_ubuntu_tag_22.04'
and img_tag
== f'{OD_VERSION}_image_nikolaik___python-nodejs_tag_python3.11-nodejs22'
)
base_image = 'ubuntu'