docker.py 6.0 KB

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