docker.py 5.6 KB

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