docker.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import docker
  2. from openhands.core.logger import openhands_logger as logger
  3. from openhands.runtime.builder.base import RuntimeBuilder
  4. class DockerRuntimeBuilder(RuntimeBuilder):
  5. def __init__(self, docker_client: docker.DockerClient):
  6. self.docker_client = docker_client
  7. def build(self, path: str, tags: list[str]) -> str:
  8. target_image_hash_name = tags[0]
  9. target_image_repo, target_image_hash_tag = target_image_hash_name.split(':')
  10. target_image_tag = tags[1].split(':')[1] if len(tags) > 1 else None
  11. try:
  12. build_logs = self.docker_client.api.build(
  13. path=path,
  14. tag=target_image_hash_name,
  15. rm=True,
  16. decode=True,
  17. )
  18. except docker.errors.BuildError as e:
  19. logger.error(f'Sandbox image build failed: {e}')
  20. raise RuntimeError(f'Sandbox image build failed: {e}')
  21. for log in build_logs:
  22. if 'stream' in log:
  23. logger.info(log['stream'].strip())
  24. elif 'error' in log:
  25. logger.error(log['error'].strip())
  26. else:
  27. logger.info(str(log))
  28. logger.info(f'Image [{target_image_hash_name}] build finished.')
  29. assert (
  30. target_image_tag
  31. ), f'Expected target image tag [{target_image_tag}] is None'
  32. image = self.docker_client.images.get(target_image_hash_name)
  33. image.tag(target_image_repo, target_image_tag)
  34. logger.info(
  35. f'Re-tagged image [{target_image_hash_name}] with more generic tag [{target_image_tag}]'
  36. )
  37. # Check if the image is built successfully
  38. image = self.docker_client.images.get(target_image_hash_name)
  39. if image is None:
  40. raise RuntimeError(
  41. f'Build failed: Image {target_image_hash_name} not found'
  42. )
  43. tags_str = (
  44. f'{target_image_hash_tag}, {target_image_tag}'
  45. if target_image_tag
  46. else target_image_hash_tag
  47. )
  48. logger.info(
  49. f'Image {target_image_repo} with tags [{tags_str}] built successfully'
  50. )
  51. return target_image_hash_name
  52. def image_exists(self, image_name: str) -> bool:
  53. """Check if the image exists in the registry (try to pull it first) or in the local store.
  54. Args:
  55. image_name (str): The Docker image to check (<image repo>:<image tag>)
  56. Returns:
  57. bool: Whether the Docker image exists in the registry or in the local store
  58. """
  59. try:
  60. logger.info(f'Checking, if image exists locally:\n{image_name}')
  61. self.docker_client.images.get(image_name)
  62. logger.info('Image found locally.')
  63. return True
  64. except docker.errors.ImageNotFound:
  65. try:
  66. logger.info(
  67. 'Image not found locally. Trying to pull it, please wait...'
  68. )
  69. layers = {}
  70. for line in self.docker_client.api.pull(
  71. image_name, stream=True, decode=True
  72. ):
  73. if 'id' in line and 'progressDetail' in line:
  74. layer_id = line['id']
  75. if layer_id not in layers:
  76. layers[layer_id] = {
  77. 'last_logged': -10
  78. } # Initialize last logged at -10%
  79. if (
  80. 'total' in line['progressDetail']
  81. and 'current' in line['progressDetail']
  82. ):
  83. total = line['progressDetail']['total']
  84. current = line['progressDetail']['current']
  85. percentage = (current / total) * 100
  86. # Log if percentage is at least 10% higher than last logged
  87. if percentage - layers[layer_id]['last_logged'] >= 10:
  88. logger.info(
  89. f'Layer {layer_id}: {percentage:.0f}% downloaded'
  90. )
  91. layers[layer_id]['last_logged'] = percentage
  92. elif 'status' in line:
  93. logger.info(line['status'])
  94. logger.info('Image pulled')
  95. return True
  96. except docker.errors.ImageNotFound:
  97. logger.info('Could not find image locally or in registry.')
  98. return False
  99. except Exception as e:
  100. msg = 'Image could not be pulled: '
  101. ex_msg = str(e)
  102. if 'Not Found' in ex_msg:
  103. msg += 'image not found in registry.'
  104. else:
  105. msg += f'{ex_msg}'
  106. logger.warning(msg)
  107. return False