image_agnostic_util.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import tempfile
  2. import docker
  3. from opendevin.core.logger import opendevin_logger as logger
  4. def generate_dockerfile_content(base_image: str) -> str:
  5. """
  6. Generate the Dockerfile content for the agnostic sandbox image based on user-provided base image.
  7. NOTE: This is only tested on debian yet.
  8. """
  9. # FIXME: Remove the requirement of ssh in future version
  10. dockerfile_content = (
  11. f'FROM {base_image}\n'
  12. 'RUN apt update && apt install -y openssh-server wget sudo\n'
  13. 'RUN mkdir -p -m0755 /var/run/sshd\n'
  14. 'RUN mkdir -p /opendevin && mkdir -p /opendevin/logs && chmod 777 /opendevin/logs\n'
  15. 'RUN wget "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"\n'
  16. 'RUN bash Miniforge3-$(uname)-$(uname -m).sh -b -p /opendevin/miniforge3\n'
  17. 'RUN bash -c ". /opendevin/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"\n'
  18. 'RUN echo "export PATH=/opendevin/miniforge3/bin:$PATH" >> ~/.bashrc\n'
  19. 'RUN echo "export PATH=/opendevin/miniforge3/bin:$PATH" >> /opendevin/bash.bashrc\n'
  20. ).strip()
  21. return dockerfile_content
  22. def _build_sandbox_image(
  23. base_image: str, target_image_name: str, docker_client: docker.DockerClient
  24. ):
  25. try:
  26. with tempfile.TemporaryDirectory() as temp_dir:
  27. dockerfile_content = generate_dockerfile_content(base_image)
  28. logger.info(f'Building agnostic sandbox image: {target_image_name}')
  29. logger.info(
  30. (
  31. f'===== Dockerfile content =====\n'
  32. f'{dockerfile_content}\n'
  33. f'==============================='
  34. )
  35. )
  36. with open(f'{temp_dir}/Dockerfile', 'w') as file:
  37. file.write(dockerfile_content)
  38. image, logs = docker_client.images.build(
  39. path=temp_dir, tag=target_image_name
  40. )
  41. for log in logs:
  42. if 'stream' in log:
  43. print(log['stream'].strip())
  44. logger.info(f'Image {image} built successfully')
  45. except docker.errors.BuildError as e:
  46. logger.error(f'Sandbox image build failed: {e}')
  47. raise e
  48. except Exception as e:
  49. logger.error(f'An error occurred during sandbox image build: {e}')
  50. raise e
  51. def _get_new_image_name(base_image: str) -> str:
  52. if ':' not in base_image:
  53. base_image = base_image + ':latest'
  54. [repo, tag] = base_image.split(':')
  55. repo = repo.replace('/', '___')
  56. return f'od_sandbox:{repo}__{tag}'
  57. def get_od_sandbox_image(base_image: str, docker_client: docker.DockerClient) -> str:
  58. """Return the sandbox image name based on user-provided base image.
  59. The returned sandbox image is assumed to contains all the required dependencies for OpenDevin.
  60. If the sandbox image is not found, it will be built.
  61. """
  62. # OpenDevin's offcial sandbox already contains the required dependencies for OpenDevin.
  63. if 'ghcr.io/opendevin/sandbox' in base_image:
  64. return base_image
  65. new_image_name = _get_new_image_name(base_image)
  66. # Detect if the sandbox image is built
  67. images = docker_client.images.list()
  68. for image in images:
  69. if new_image_name in image.tags:
  70. logger.info('Found existing od_sandbox image, reuse:' + new_image_name)
  71. return new_image_name
  72. # If the sandbox image is not found, build it
  73. logger.info(
  74. f'od_sandbox image is not found for {base_image}, will build: {new_image_name}'
  75. )
  76. _build_sandbox_image(base_image, new_image_name, docker_client)
  77. return new_image_name