remote.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import base64
  2. import io
  3. import tarfile
  4. import time
  5. import requests
  6. from openhands.core.logger import openhands_logger as logger
  7. from openhands.runtime.builder import RuntimeBuilder
  8. class RemoteRuntimeBuilder(RuntimeBuilder):
  9. """This class interacts with the remote Runtime API for building and managing container images."""
  10. def __init__(self, api_url: str, api_key: str):
  11. self.api_url = api_url
  12. self.api_key = api_key
  13. def build(self, path: str, tags: list[str]) -> str:
  14. """Builds a Docker image using the Runtime API's /build endpoint."""
  15. # Create a tar archive of the build context
  16. tar_buffer = io.BytesIO()
  17. with tarfile.open(fileobj=tar_buffer, mode='w:gz') as tar:
  18. tar.add(path, arcname='.')
  19. tar_buffer.seek(0)
  20. # Encode the tar file as base64
  21. base64_encoded_tar = base64.b64encode(tar_buffer.getvalue()).decode('utf-8')
  22. # Prepare the multipart form data
  23. files = [
  24. ('context', ('context.tar.gz', base64_encoded_tar)),
  25. ('target_image', (None, tags[0])),
  26. ]
  27. # Add additional tags if present
  28. for tag in tags[1:]:
  29. files.append(('tags', (None, tag)))
  30. # Send the POST request to /build
  31. headers = {'X-API-Key': self.api_key}
  32. response = requests.post(f'{self.api_url}/build', files=files, headers=headers)
  33. if response.status_code != 202:
  34. logger.error(f'Build initiation failed: {response.text}')
  35. raise RuntimeError(f'Build initiation failed: {response.text}')
  36. build_data = response.json()
  37. build_id = build_data['build_id']
  38. logger.info(f'Build initiated with ID: {build_id}')
  39. # Poll /build_status until the build is complete
  40. start_time = time.time()
  41. timeout = 30 * 60 # 20 minutes in seconds
  42. while True:
  43. if time.time() - start_time > timeout:
  44. logger.error('Build timed out after 30 minutes')
  45. raise RuntimeError('Build timed out after 30 minutes')
  46. status_response = requests.get(
  47. f'{self.api_url}/build_status',
  48. params={'build_id': build_id},
  49. headers=headers,
  50. )
  51. if status_response.status_code != 200:
  52. logger.error(f'Failed to get build status: {status_response.text}')
  53. raise RuntimeError(
  54. f'Failed to get build status: {status_response.text}'
  55. )
  56. status_data = status_response.json()
  57. status = status_data['status']
  58. logger.info(f'Build status: {status}')
  59. if status == 'SUCCESS':
  60. logger.info(f"Successfully built {status_data['image']}")
  61. return status_data['image']
  62. elif status in [
  63. 'FAILURE',
  64. 'INTERNAL_ERROR',
  65. 'TIMEOUT',
  66. 'CANCELLED',
  67. 'EXPIRED',
  68. ]:
  69. error_message = status_data.get(
  70. 'error', f'Build failed with status: {status}'
  71. )
  72. logger.error(error_message)
  73. raise RuntimeError(error_message)
  74. # Wait before polling again
  75. time.sleep(5)
  76. def image_exists(self, image_name: str) -> bool:
  77. """Checks if an image exists in the remote registry using the /image_exists endpoint."""
  78. params = {'image': image_name}
  79. session = requests.Session()
  80. session.headers.update({'X-API-Key': self.api_key})
  81. response = session.get(f'{self.api_url}/image_exists', params=params)
  82. if response.status_code != 200:
  83. logger.error(f'Failed to check image existence: {response.text}')
  84. raise RuntimeError(f'Failed to check image existence: {response.text}')
  85. result = response.json()
  86. if result['exists']:
  87. logger.info(
  88. f"Image {image_name} exists. "
  89. f"Uploaded at: {result['image']['upload_time']}, "
  90. f"Size: {result['image']['image_size_bytes'] / 1024 / 1024:.2f} MB"
  91. )
  92. else:
  93. logger.info(f'Image {image_name} does not exist.')
  94. return result['exists']