mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
[Arch] Add runtime image build CI & clean up runtime build using jinja2 template (#3055)
* test_runtime_client.py to test _execute_bash() * runtime_build and runtime tweaks * fix in docker script * revert bash changes * use sandbox_config.update_source_code to control source code update * add od_version to the sandbox tag * add doc instruction for update source code * do not remove whole poetry folder; add mamba clean * add missing newlines * cleanup runtime dockerfile into jinja template * make prep temp file a separate function; make that function accessible through cli * modify `runtime_build.py` so it can generate directory for building docker img * add dockerfile and sdist of runtime to gitignore since it will be dynamically generated * add runtime to build * do not rebuild new image when an `od_runtime` is provided * use default container_image for testing if possible * move runtime tests to ghcr runtime workflow * update docker base dir for runtime * fix unittest * fix image name * fix image name for test case * rename to make it consistent --------- Co-authored-by: tobitege <tobitege@gmx.de>
This commit is contained in:
parent
547c510848
commit
405c8a0456
242
.github/workflows/ghcr-runtime.yml
vendored
Normal file
242
.github/workflows/ghcr-runtime.yml
vendored
Normal file
@ -0,0 +1,242 @@
|
||||
name: Build Publish and Test Runtime Image
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
required: true
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
ghcr_build_runtime:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
tags: ${{ steps.capture-tags.outputs.tags }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
image: ["od_runtime"]
|
||||
base_image: ["ubuntu:22.04"]
|
||||
platform: ["amd64", "arm64"]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: false
|
||||
swap-storage: true
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: "poetry"
|
||||
|
||||
- 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
|
||||
|
||||
- name: Build and export image
|
||||
id: build
|
||||
run: ./containers/build.sh ${{ matrix.image }} ${{ github.repository_owner }} ${{ matrix.platform }}
|
||||
|
||||
- name: Capture tags
|
||||
id: capture-tags
|
||||
run: |
|
||||
tags=$(cat tags.txt)
|
||||
echo "tags=$tags"
|
||||
echo "tags=$tags" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload Docker image as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.image }}-docker-image-${{ matrix.platform }}
|
||||
path: /tmp/${{ matrix.image }}_image_${{ matrix.platform }}.tar
|
||||
|
||||
test-for-runtime:
|
||||
name: Test for Runtime
|
||||
runs-on: ubuntu-latest
|
||||
needs: ghcr_build_runtime
|
||||
env:
|
||||
PERSIST_SANDBOX: "false"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# when set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: "poetry"
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
|
||||
- name: Download Runtime Docker image
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: od_runtime-docker-image-amd64
|
||||
path: /tmp/
|
||||
|
||||
- name: Load Runtime image and run runtime tests
|
||||
run: |
|
||||
# Load the Docker image and capture the output
|
||||
output=$(docker load -i /tmp/od_runtime_image_amd64.tar)
|
||||
|
||||
# Extract the first image name from the output
|
||||
image_name=$(echo "$output" | grep -oP 'Loaded image: \K.*' | head -n 1)
|
||||
|
||||
# Print the full name of the image
|
||||
echo "Loaded Docker image: $image_name"
|
||||
|
||||
SANDBOX_CONTAINER_IMAGE=$image_name TEST_IN_CI=true poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml -s ./tests/unit/test_runtime.py
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
ghcr_push:
|
||||
runs-on: ubuntu-latest
|
||||
# don't push if runtime tests fail
|
||||
needs: [ghcr_build_runtime, test-for-runtime]
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
env:
|
||||
tags: ${{ needs.ghcr_build_runtime.outputs.tags }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
image: ["od_runtime"]
|
||||
platform: ["amd64", "arm64"]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download Docker images
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.image }}-docker-image-${{ matrix.platform }}
|
||||
path: /tmp/${{ matrix.platform }}
|
||||
|
||||
- name: Load images and push to registry
|
||||
run: |
|
||||
mv /tmp/${{ matrix.platform }}/${{ matrix.image }}_image_${{ matrix.platform }}.tar .
|
||||
loaded_image=$(docker load -i ${{ matrix.image }}_image_${{ matrix.platform }}.tar | grep "Loaded image:" | head -n 1 | awk '{print $3}')
|
||||
echo "loaded image = $loaded_image"
|
||||
tags=$(echo ${tags} | tr ' ' '\n')
|
||||
image_name=$(echo "ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}" | tr '[:upper:]' '[:lower:]')
|
||||
echo "image name = $image_name"
|
||||
for tag in $tags; do
|
||||
echo "tag = $tag"
|
||||
docker tag $loaded_image $image_name:${tag}_${{ matrix.platform }}
|
||||
docker push $image_name:${tag}_${{ matrix.platform }}
|
||||
done
|
||||
|
||||
create_manifest:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ghcr_build_runtime, ghcr_push]
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
env:
|
||||
tags: ${{ needs.ghcr_build_runtime.outputs.tags }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
image: ["od_runtime"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create and push multi-platform manifest
|
||||
run: |
|
||||
image_name=$(echo "ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}" | tr '[:upper:]' '[:lower:]')
|
||||
echo "image name = $image_name"
|
||||
tags=$(echo ${tags} | tr ' ' '\n')
|
||||
for tag in $tags; do
|
||||
echo 'tag = $tag'
|
||||
docker buildx imagetools create --tag $image_name:$tag \
|
||||
$image_name:${tag}_amd64 \
|
||||
$image_name:${tag}_arm64
|
||||
done
|
||||
64
.github/workflows/run-runtime-tests.yml
vendored
64
.github/workflows/run-runtime-tests.yml
vendored
@ -1,64 +0,0 @@
|
||||
name: Run Runtime Tests
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'frontend/**'
|
||||
- 'docs/**'
|
||||
- 'evaluation/**'
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
PERSIST_SANDBOX : "false"
|
||||
|
||||
jobs:
|
||||
test-for-runtime:
|
||||
name: Test for Runtime
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PERSIST_SANDBOX: "false"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# when set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: "poetry"
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
|
||||
- name: Run runtime tests
|
||||
run: |
|
||||
TEST_IN_CI=true poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml -s ./tests/unit/test_runtime.py
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -220,3 +220,7 @@ image_build_logs
|
||||
run_instance_logs
|
||||
|
||||
od_runtime_*.tar
|
||||
|
||||
# docker build
|
||||
containers/runtime/Dockerfile
|
||||
containers/runtime/project.tar.gz
|
||||
|
||||
@ -27,11 +27,14 @@ echo "Tags: ${tags[@]}"
|
||||
|
||||
if [[ "$image_name" == "opendevin" ]]; then
|
||||
dir="./containers/app"
|
||||
elif [[ "$image_name" == "od_runtime" ]]; then
|
||||
dir="./containers/runtime"
|
||||
else
|
||||
dir="./containers/$image_name"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$dir/Dockerfile" ]]; then
|
||||
if [[ (! -f "$dir/Dockerfile") && "$image_name" != "od_runtime" ]]; then
|
||||
# Allow runtime to be built without a Dockerfile
|
||||
echo "No Dockerfile found"
|
||||
exit 1
|
||||
fi
|
||||
@ -46,6 +49,11 @@ if [[ -n "$org_name" ]]; then
|
||||
DOCKER_ORG="$org_name"
|
||||
fi
|
||||
|
||||
# If $DOCKER_IMAGE_TAG is set, add it to the tags
|
||||
if [[ -n "$DOCKER_IMAGE_TAG" ]]; then
|
||||
tags+=("$DOCKER_IMAGE_TAG")
|
||||
fi
|
||||
|
||||
DOCKER_REPOSITORY="$DOCKER_REGISTRY/$DOCKER_ORG/$DOCKER_IMAGE"
|
||||
DOCKER_REPOSITORY=${DOCKER_REPOSITORY,,} # lowercase
|
||||
echo "Repo: $DOCKER_REPOSITORY"
|
||||
|
||||
11
containers/runtime/README.md
Normal file
11
containers/runtime/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Dynamic constructed Dockerfile
|
||||
|
||||
This folder builds runtime image (sandbox), which will use a `Dockerfile` that is dynamically generated depends on the `base_image` AND a [Python source distribution](https://docs.python.org/3.10/distutils/sourcedist.html) that's based on the current commit of `opendevin`.
|
||||
|
||||
The following command will generate Dockerfile for `ubuntu:22.04` and the source distribution `.tar` into `containers/runtime`.
|
||||
|
||||
```bash
|
||||
poetry run python3 opendevin/runtime/utils/runtime_build.py \
|
||||
--base_image ubuntu:22.04 \
|
||||
--build_folder containers/runtime
|
||||
```
|
||||
8
containers/runtime/config.sh
Normal file
8
containers/runtime/config.sh
Normal file
@ -0,0 +1,8 @@
|
||||
DOCKER_REGISTRY=ghcr.io
|
||||
DOCKER_ORG=opendevin
|
||||
DOCKER_BASE_DIR="./containers/runtime"
|
||||
# These two variables will be appended by the runtime_build.py script
|
||||
# DOCKER_IMAGE=
|
||||
# DOCKER_IMAGE_TAG=
|
||||
DOCKER_IMAGE=od_runtime
|
||||
DOCKER_IMAGE_TAG=od_v0.8.1_image_ubuntu_tag_22.04
|
||||
@ -6,6 +6,7 @@ import tempfile
|
||||
|
||||
import docker
|
||||
import toml
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
import opendevin
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
@ -64,75 +65,42 @@ def _put_source_code_to_dir(temp_dir: str) -> str:
|
||||
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:
|
||||
# Ubuntu 22.x has libgl1-mesa-glx, but 24.x and above have libgl1!
|
||||
if 'ubuntu' in base_image and (
|
||||
base_image.endswith(':latest') or base_image.endswith(':24.04')
|
||||
):
|
||||
LIBGL_MESA = 'libgl1'
|
||||
else:
|
||||
LIBGL_MESA = 'libgl1-mesa-glx'
|
||||
|
||||
dockerfile_content = (
|
||||
f'FROM {base_image}\n'
|
||||
# Install necessary packages and clean up in one layer
|
||||
f'RUN apt-get update && apt-get install -y wget sudo apt-utils {LIBGL_MESA} libasound2-plugins && \\\n'
|
||||
f' apt-get clean && rm -rf /var/lib/apt/lists/*\n'
|
||||
# Create necessary directories
|
||||
f'RUN mkdir -p /opendevin && mkdir -p /opendevin/logs && chmod 777 /opendevin/logs && \\\n'
|
||||
f' echo "" > /opendevin/bash.bashrc\n'
|
||||
# Install Miniforge3
|
||||
f'RUN if [ ! -d /opendevin/miniforge3 ]; then \\\n'
|
||||
f' wget --progress=bar:force -O Miniforge3.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh" && \\\n'
|
||||
f' bash Miniforge3.sh -b -p /opendevin/miniforge3 && \\\n'
|
||||
f' rm Miniforge3.sh && \\\n'
|
||||
f' chmod -R g+w /opendevin/miniforge3 && \\\n'
|
||||
f' bash -c ". /opendevin/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"; \\\n'
|
||||
f' fi\n'
|
||||
'RUN /opendevin/miniforge3/bin/mamba install python=3.11 -y\n'
|
||||
'RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry -y\n'
|
||||
"""Generate the Dockerfile content for the eventstream runtime image based on user-provided base image."""
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(
|
||||
searchpath=os.path.join(os.path.dirname(__file__), 'runtime_templates')
|
||||
)
|
||||
|
||||
# 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'
|
||||
|
||||
# ALTERNATIVE, but maybe not complete? (toml error!)
|
||||
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 --no-interaction --no-root\n'
|
||||
'RUN /opendevin/miniforge3/bin/mamba run -n base poetry cache clear --all . && \\\n'
|
||||
'apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* &&\\\n'
|
||||
'/opendevin/miniforge3/bin/mamba clean --all\n'
|
||||
)
|
||||
|
||||
# For browser (update if needed)
|
||||
dockerfile_content += (
|
||||
'RUN apt-get update && \\\n'
|
||||
' cd /opendevin/code && \\\n'
|
||||
' /opendevin/miniforge3/bin/mamba run -n base poetry run pip install playwright && \\\n'
|
||||
' /opendevin/miniforge3/bin/mamba run -n base poetry run playwright install --with-deps chromium && \\\n'
|
||||
' apt-get clean && \\\n'
|
||||
' rm -rf /var/lib/apt/lists/*\n'
|
||||
template = env.get_template('Dockerfile.j2')
|
||||
dockerfile_content = template.render(
|
||||
base_image=base_image,
|
||||
source_code_dirname=source_code_dirname,
|
||||
skip_init=skip_init,
|
||||
)
|
||||
return dockerfile_content
|
||||
|
||||
|
||||
def prep_docker_build_folder(
|
||||
dir_path: str,
|
||||
base_image: str,
|
||||
skip_init: bool = False,
|
||||
):
|
||||
"""Prepares the docker build folder by copying the source code and generating the Dockerfile."""
|
||||
source_code_dirname = _put_source_code_to_dir(dir_path)
|
||||
dockerfile_content = _generate_dockerfile(
|
||||
base_image, source_code_dirname, skip_init=skip_init
|
||||
)
|
||||
logger.info(
|
||||
(
|
||||
f'===== Dockerfile content =====\n'
|
||||
f'{dockerfile_content}\n'
|
||||
f'==============================='
|
||||
)
|
||||
)
|
||||
with open(os.path.join(dir_path, 'Dockerfile'), 'w') as file:
|
||||
file.write(dockerfile_content)
|
||||
|
||||
|
||||
def _build_sandbox_image(
|
||||
base_image: str,
|
||||
target_image_name: str,
|
||||
@ -141,26 +109,13 @@ def _build_sandbox_image(
|
||||
):
|
||||
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)
|
||||
|
||||
prep_docker_build_folder(temp_dir, base_image, skip_init=skip_init)
|
||||
api_client = docker_client.api
|
||||
build_logs = api_client.build(
|
||||
path=temp_dir,
|
||||
@ -193,7 +148,7 @@ def _build_sandbox_image(
|
||||
raise e
|
||||
|
||||
|
||||
def _get_new_image_name(base_image: str, dev_mode: bool = False) -> str:
|
||||
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(
|
||||
@ -201,6 +156,10 @@ def _get_new_image_name(base_image: str, dev_mode: bool = False) -> str:
|
||||
)
|
||||
# remove the 'od_runtime' prefix from the base_image
|
||||
return base_image.replace('od_runtime', 'od_runtime_dev')
|
||||
elif 'od_runtime' in base_image:
|
||||
# if the base image is a valid od_runtime image, we will use it as is
|
||||
logger.info(f'Using existing od_runtime image [{base_image}]')
|
||||
return base_image
|
||||
else:
|
||||
prefix = 'od_runtime'
|
||||
if ':' not in base_image:
|
||||
@ -231,8 +190,13 @@ def build_runtime_image(
|
||||
|
||||
This is only used for **eventstream runtime**.
|
||||
"""
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
logger.info(f'New image name: {new_image_name}')
|
||||
new_image_name = get_new_image_name(base_image)
|
||||
if base_image == new_image_name:
|
||||
logger.info(
|
||||
f'Using existing od_runtime image [{base_image}]. Will NOT build a new image.'
|
||||
)
|
||||
else:
|
||||
logger.info(f'New image name: {new_image_name}')
|
||||
|
||||
# Ensure new_image_name contains a colon
|
||||
if ':' not in new_image_name:
|
||||
@ -264,7 +228,7 @@ def build_runtime_image(
|
||||
# e.g., od_runtime:ubuntu_tag_latest -> od_runtime_dev:ubuntu_tag_latest
|
||||
logger.info('Image exists, but updating source code requested')
|
||||
base_image = new_image_name
|
||||
new_image_name = _get_new_image_name(base_image, dev_mode=True)
|
||||
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:
|
||||
@ -302,15 +266,42 @@ def build_runtime_image(
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--base_image', type=str, default='ubuntu:22.04')
|
||||
parser.add_argument('--update_source_code', type=bool, default=False)
|
||||
parser.add_argument('--save_to_local_store', type=bool, default=False)
|
||||
parser.add_argument('--update_source_code', action='store_true')
|
||||
parser.add_argument('--save_to_local_store', action='store_true')
|
||||
parser.add_argument('--build_folder', type=str, default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
client = docker.from_env()
|
||||
image_name = build_runtime_image(
|
||||
args.base_image,
|
||||
client,
|
||||
update_source_code=args.update_source_code,
|
||||
save_to_local_store=args.save_to_local_store,
|
||||
)
|
||||
print(f'\nBUILT Image: {image_name}\n')
|
||||
if args.build_folder is not None:
|
||||
build_folder = args.build_folder
|
||||
assert os.path.exists(
|
||||
build_folder
|
||||
), f'Build folder {build_folder} does not exist'
|
||||
logger.info(
|
||||
f'Will prepare a build folder by copying the source code and generating the Dockerfile: {build_folder}'
|
||||
)
|
||||
new_image_path = get_new_image_name(args.base_image)
|
||||
prep_docker_build_folder(
|
||||
build_folder, args.base_image, skip_init=args.update_source_code
|
||||
)
|
||||
new_image_name, new_image_tag = new_image_path.split(':')
|
||||
with open(os.path.join(build_folder, 'config.sh'), 'a') as file:
|
||||
file.write(
|
||||
(
|
||||
f'DOCKER_IMAGE={new_image_name}\n'
|
||||
f'DOCKER_IMAGE_TAG={new_image_tag}\n'
|
||||
)
|
||||
)
|
||||
logger.info(
|
||||
f'`config.sh` is updated with the new image name [{new_image_name}] and tag [{new_image_tag}]'
|
||||
)
|
||||
logger.info(f'Dockerfile and source distribution are ready in {build_folder}')
|
||||
else:
|
||||
logger.info('Building image in a temporary folder')
|
||||
client = docker.from_env()
|
||||
image_name = build_runtime_image(
|
||||
args.base_image,
|
||||
client,
|
||||
update_source_code=args.update_source_code,
|
||||
save_to_local_store=args.save_to_local_store,
|
||||
)
|
||||
print(f'\nBUILT Image: {image_name}\n')
|
||||
|
||||
66
opendevin/runtime/utils/runtime_templates/Dockerfile.j2
Normal file
66
opendevin/runtime/utils/runtime_templates/Dockerfile.j2
Normal file
@ -0,0 +1,66 @@
|
||||
{% if skip_init %}
|
||||
FROM {{ base_image }}
|
||||
{% else %}
|
||||
# ================================================================
|
||||
# START: Build Runtime Image from Scratch
|
||||
# ================================================================
|
||||
FROM {{ base_image }}
|
||||
{% if 'ubuntu' in base_image and (base_image.endswith(':latest') or base_image.endswith(':24.04')) %}
|
||||
{% set LIBGL_MESA = 'libgl1' %}
|
||||
{% else %}
|
||||
{% set LIBGL_MESA = 'libgl1-mesa-glx' %}
|
||||
{% endif %}
|
||||
|
||||
# Install necessary packages and clean up in one layer
|
||||
RUN apt-get update && \
|
||||
apt-get install -y wget sudo apt-utils {{ LIBGL_MESA }} libasound2-plugins && \
|
||||
apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /opendevin && \
|
||||
mkdir -p /opendevin/logs && \
|
||||
chmod 777 /opendevin/logs && \
|
||||
echo "" > /opendevin/bash.bashrc
|
||||
|
||||
RUN if [ ! -d /opendevin/miniforge3 ]; then \
|
||||
wget --progress=bar:force -O Miniforge3.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh" && \
|
||||
bash Miniforge3.sh -b -p /opendevin/miniforge3 && \
|
||||
rm Miniforge3.sh && \
|
||||
chmod -R g+w /opendevin/miniforge3 && \
|
||||
bash -c ". /opendevin/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"; \
|
||||
fi
|
||||
|
||||
# Install Python and Poetry
|
||||
RUN /opendevin/miniforge3/bin/mamba install python=3.11 -y
|
||||
RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry -y
|
||||
# ================================================================
|
||||
# END: Build Runtime Image from Scratch
|
||||
# ================================================================
|
||||
{% endif %}
|
||||
|
||||
# ================================================================
|
||||
# START: Copy Project and Install/Update Dependencies
|
||||
# ================================================================
|
||||
COPY project.tar.gz /opendevin
|
||||
RUN if [ -d /opendevin/code ]; then rm -rf /opendevin/code; fi
|
||||
RUN cd /opendevin && tar -xzvf project.tar.gz && rm project.tar.gz
|
||||
RUN mv /opendevin/{{ source_code_dirname }} /opendevin/code
|
||||
|
||||
# Install/Update Dependencies
|
||||
# 1. Install pyproject.toml via poetry
|
||||
# 2. Install playwright and chromium
|
||||
# 3. Clear poetry, apt, mamba caches
|
||||
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 --no-interaction --no-root && \
|
||||
apt-get update && \
|
||||
/opendevin/miniforge3/bin/mamba run -n base poetry run pip install playwright && \
|
||||
/opendevin/miniforge3/bin/mamba run -n base poetry run playwright install --with-deps chromium && \
|
||||
/opendevin/miniforge3/bin/mamba run -n base poetry cache clear --all . && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
|
||||
/opendevin/miniforge3/bin/mamba clean --all
|
||||
|
||||
# ================================================================
|
||||
# END: Copy Project and Install/Update Dependencies
|
||||
# ================================================================
|
||||
@ -8,6 +8,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from opendevin.core.config import SandboxConfig
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.events import EventStream
|
||||
from opendevin.events.action import (
|
||||
CmdRunAction,
|
||||
@ -32,6 +33,14 @@ async def _load_runtime(box_class, event_stream, plugins, sid):
|
||||
sandbox_config = SandboxConfig(
|
||||
use_host_network=False,
|
||||
)
|
||||
container_image = sandbox_config.container_image
|
||||
# NOTE: we will use the default container image specified in the config.sandbox
|
||||
# if it is an official od_runtime image.
|
||||
if 'od_runtime' not in container_image:
|
||||
container_image = 'ubuntu:22.04'
|
||||
logger.warning(
|
||||
f'`sandbox_config.container_image` is not an od_runtime image. Will use `{container_image}` as the container image for testing.'
|
||||
)
|
||||
if box_class == EventStreamRuntime:
|
||||
runtime = EventStreamRuntime(
|
||||
sandbox_config=sandbox_config,
|
||||
@ -39,7 +48,7 @@ async def _load_runtime(box_class, event_stream, plugins, sid):
|
||||
sid=sid,
|
||||
# NOTE: we probably don't have a default container image `/sandbox` for the event stream runtime
|
||||
# Instead, we will pre-build a suite of container images with OD-runtime-cli installed.
|
||||
container_image='ubuntu:22.04',
|
||||
container_image=container_image,
|
||||
plugins=plugins,
|
||||
)
|
||||
await runtime.ainit()
|
||||
|
||||
@ -9,9 +9,9 @@ import toml
|
||||
|
||||
from opendevin.runtime.utils.runtime_build import (
|
||||
_generate_dockerfile,
|
||||
_get_new_image_name,
|
||||
_put_source_code_to_dir,
|
||||
build_runtime_image,
|
||||
get_new_image_name,
|
||||
)
|
||||
|
||||
RUNTIME_IMAGE_PREFIX = 'od_runtime'
|
||||
@ -95,44 +95,44 @@ def test_generate_dockerfile_skip_init():
|
||||
|
||||
def test_get_new_image_name_eventstream():
|
||||
base_image = 'debian:11'
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
get_new_image_name(base_image, dev_mode=True)
|
||||
|
||||
|
||||
@patch('opendevin.runtime.utils.runtime_build._build_sandbox_image')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user