pybiolib 0.2.951__py3-none-any.whl → 1.2.1890__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. biolib/__init__.py +357 -11
  2. biolib/_data_record/data_record.py +380 -0
  3. biolib/_index/__init__.py +0 -0
  4. biolib/_index/index.py +55 -0
  5. biolib/_index/query_result.py +103 -0
  6. biolib/_internal/__init__.py +0 -0
  7. biolib/_internal/add_copilot_prompts.py +58 -0
  8. biolib/_internal/add_gui_files.py +81 -0
  9. biolib/_internal/data_record/__init__.py +1 -0
  10. biolib/_internal/data_record/data_record.py +85 -0
  11. biolib/_internal/data_record/push_data.py +116 -0
  12. biolib/_internal/data_record/remote_storage_endpoint.py +43 -0
  13. biolib/_internal/errors.py +5 -0
  14. biolib/_internal/file_utils.py +125 -0
  15. biolib/_internal/fuse_mount/__init__.py +1 -0
  16. biolib/_internal/fuse_mount/experiment_fuse_mount.py +209 -0
  17. biolib/_internal/http_client.py +159 -0
  18. biolib/_internal/lfs/__init__.py +1 -0
  19. biolib/_internal/lfs/cache.py +51 -0
  20. biolib/_internal/libs/__init__.py +1 -0
  21. biolib/_internal/libs/fusepy/__init__.py +1257 -0
  22. biolib/_internal/push_application.py +488 -0
  23. biolib/_internal/runtime.py +22 -0
  24. biolib/_internal/string_utils.py +13 -0
  25. biolib/_internal/templates/__init__.py +1 -0
  26. biolib/_internal/templates/copilot_template/.github/instructions/general-app-knowledge.instructions.md +10 -0
  27. biolib/_internal/templates/copilot_template/.github/instructions/style-general.instructions.md +20 -0
  28. biolib/_internal/templates/copilot_template/.github/instructions/style-python.instructions.md +16 -0
  29. biolib/_internal/templates/copilot_template/.github/instructions/style-react-ts.instructions.md +47 -0
  30. biolib/_internal/templates/copilot_template/.github/prompts/biolib_app_inputs.prompt.md +11 -0
  31. biolib/_internal/templates/copilot_template/.github/prompts/biolib_onboard_repo.prompt.md +19 -0
  32. biolib/_internal/templates/copilot_template/.github/prompts/biolib_run_apps.prompt.md +12 -0
  33. biolib/_internal/templates/dashboard_template/.biolib/config.yml +5 -0
  34. biolib/_internal/templates/github_workflow_template/.github/workflows/biolib.yml +21 -0
  35. biolib/_internal/templates/gitignore_template/.gitignore +10 -0
  36. biolib/_internal/templates/gui_template/.yarnrc.yml +1 -0
  37. biolib/_internal/templates/gui_template/App.tsx +53 -0
  38. biolib/_internal/templates/gui_template/Dockerfile +27 -0
  39. biolib/_internal/templates/gui_template/biolib-sdk.ts +82 -0
  40. biolib/_internal/templates/gui_template/dev-data/output.json +7 -0
  41. biolib/_internal/templates/gui_template/index.css +5 -0
  42. biolib/_internal/templates/gui_template/index.html +13 -0
  43. biolib/_internal/templates/gui_template/index.tsx +10 -0
  44. biolib/_internal/templates/gui_template/package.json +27 -0
  45. biolib/_internal/templates/gui_template/tsconfig.json +24 -0
  46. biolib/_internal/templates/gui_template/vite-plugin-dev-data.ts +50 -0
  47. biolib/_internal/templates/gui_template/vite.config.mts +10 -0
  48. biolib/_internal/templates/init_template/.biolib/config.yml +19 -0
  49. biolib/_internal/templates/init_template/Dockerfile +14 -0
  50. biolib/_internal/templates/init_template/requirements.txt +1 -0
  51. biolib/_internal/templates/init_template/run.py +12 -0
  52. biolib/_internal/templates/init_template/run.sh +4 -0
  53. biolib/_internal/templates/templates.py +25 -0
  54. biolib/_internal/tree_utils.py +106 -0
  55. biolib/_internal/utils/__init__.py +65 -0
  56. biolib/_internal/utils/auth.py +46 -0
  57. biolib/_internal/utils/job_url.py +33 -0
  58. biolib/_internal/utils/multinode.py +263 -0
  59. biolib/_runtime/runtime.py +157 -0
  60. biolib/_session/session.py +44 -0
  61. biolib/_shared/__init__.py +0 -0
  62. biolib/_shared/types/__init__.py +74 -0
  63. biolib/_shared/types/account.py +12 -0
  64. biolib/_shared/types/account_member.py +8 -0
  65. biolib/_shared/types/app.py +9 -0
  66. biolib/_shared/types/data_record.py +40 -0
  67. biolib/_shared/types/experiment.py +32 -0
  68. biolib/_shared/types/file_node.py +17 -0
  69. biolib/_shared/types/push.py +6 -0
  70. biolib/_shared/types/resource.py +37 -0
  71. biolib/_shared/types/resource_deploy_key.py +11 -0
  72. biolib/_shared/types/resource_permission.py +14 -0
  73. biolib/_shared/types/resource_version.py +19 -0
  74. biolib/_shared/types/result.py +14 -0
  75. biolib/_shared/types/typing.py +10 -0
  76. biolib/_shared/types/user.py +19 -0
  77. biolib/_shared/utils/__init__.py +7 -0
  78. biolib/_shared/utils/resource_uri.py +75 -0
  79. biolib/api/__init__.py +6 -0
  80. biolib/api/client.py +168 -0
  81. biolib/app/app.py +252 -49
  82. biolib/app/search_apps.py +45 -0
  83. biolib/biolib_api_client/api_client.py +126 -31
  84. biolib/biolib_api_client/app_types.py +24 -4
  85. biolib/biolib_api_client/auth.py +31 -8
  86. biolib/biolib_api_client/biolib_app_api.py +147 -52
  87. biolib/biolib_api_client/biolib_job_api.py +161 -141
  88. biolib/biolib_api_client/job_types.py +21 -5
  89. biolib/biolib_api_client/lfs_types.py +7 -23
  90. biolib/biolib_api_client/user_state.py +56 -0
  91. biolib/biolib_binary_format/__init__.py +1 -4
  92. biolib/biolib_binary_format/file_in_container.py +105 -0
  93. biolib/biolib_binary_format/module_input.py +24 -7
  94. biolib/biolib_binary_format/module_output_v2.py +149 -0
  95. biolib/biolib_binary_format/remote_endpoints.py +34 -0
  96. biolib/biolib_binary_format/remote_stream_seeker.py +59 -0
  97. biolib/biolib_binary_format/saved_job.py +3 -2
  98. biolib/biolib_binary_format/{attestation_document.py → stdout_and_stderr.py} +8 -8
  99. biolib/biolib_binary_format/system_status_update.py +3 -2
  100. biolib/biolib_binary_format/utils.py +175 -0
  101. biolib/biolib_docker_client/__init__.py +11 -2
  102. biolib/biolib_errors.py +36 -0
  103. biolib/biolib_logging.py +27 -10
  104. biolib/cli/__init__.py +38 -0
  105. biolib/cli/auth.py +46 -0
  106. biolib/cli/data_record.py +164 -0
  107. biolib/cli/index.py +32 -0
  108. biolib/cli/init.py +421 -0
  109. biolib/cli/lfs.py +101 -0
  110. biolib/cli/push.py +50 -0
  111. biolib/cli/run.py +63 -0
  112. biolib/cli/runtime.py +14 -0
  113. biolib/cli/sdk.py +16 -0
  114. biolib/cli/start.py +56 -0
  115. biolib/compute_node/cloud_utils/cloud_utils.py +110 -161
  116. biolib/compute_node/job_worker/cache_state.py +66 -88
  117. biolib/compute_node/job_worker/cache_types.py +1 -6
  118. biolib/compute_node/job_worker/docker_image_cache.py +112 -37
  119. biolib/compute_node/job_worker/executors/__init__.py +0 -3
  120. biolib/compute_node/job_worker/executors/docker_executor.py +532 -199
  121. biolib/compute_node/job_worker/executors/docker_types.py +9 -1
  122. biolib/compute_node/job_worker/executors/types.py +19 -9
  123. biolib/compute_node/job_worker/job_legacy_input_wait_timeout_thread.py +30 -0
  124. biolib/compute_node/job_worker/job_max_runtime_timer_thread.py +3 -5
  125. biolib/compute_node/job_worker/job_storage.py +108 -0
  126. biolib/compute_node/job_worker/job_worker.py +397 -212
  127. biolib/compute_node/job_worker/large_file_system.py +87 -38
  128. biolib/compute_node/job_worker/network_alloc.py +99 -0
  129. biolib/compute_node/job_worker/network_buffer.py +240 -0
  130. biolib/compute_node/job_worker/utilization_reporter_thread.py +197 -0
  131. biolib/compute_node/job_worker/utils.py +9 -24
  132. biolib/compute_node/remote_host_proxy.py +400 -98
  133. biolib/compute_node/utils.py +31 -9
  134. biolib/compute_node/webserver/compute_node_results_proxy.py +189 -0
  135. biolib/compute_node/webserver/proxy_utils.py +28 -0
  136. biolib/compute_node/webserver/webserver.py +130 -44
  137. biolib/compute_node/webserver/webserver_types.py +2 -6
  138. biolib/compute_node/webserver/webserver_utils.py +77 -12
  139. biolib/compute_node/webserver/worker_thread.py +183 -42
  140. biolib/experiments/__init__.py +0 -0
  141. biolib/experiments/experiment.py +356 -0
  142. biolib/jobs/__init__.py +1 -0
  143. biolib/jobs/job.py +741 -0
  144. biolib/jobs/job_result.py +185 -0
  145. biolib/jobs/types.py +50 -0
  146. biolib/py.typed +0 -0
  147. biolib/runtime/__init__.py +14 -0
  148. biolib/sdk/__init__.py +91 -0
  149. biolib/tables.py +34 -0
  150. biolib/typing_utils.py +2 -7
  151. biolib/user/__init__.py +1 -0
  152. biolib/user/sign_in.py +54 -0
  153. biolib/utils/__init__.py +162 -0
  154. biolib/utils/cache_state.py +94 -0
  155. biolib/utils/multipart_uploader.py +194 -0
  156. biolib/utils/seq_util.py +150 -0
  157. biolib/utils/zip/remote_zip.py +640 -0
  158. pybiolib-1.2.1890.dist-info/METADATA +41 -0
  159. pybiolib-1.2.1890.dist-info/RECORD +177 -0
  160. {pybiolib-0.2.951.dist-info → pybiolib-1.2.1890.dist-info}/WHEEL +1 -1
  161. pybiolib-1.2.1890.dist-info/entry_points.txt +2 -0
  162. README.md +0 -17
  163. biolib/app/app_result.py +0 -68
  164. biolib/app/utils.py +0 -62
  165. biolib/biolib-js/0-biolib.worker.js +0 -1
  166. biolib/biolib-js/1-biolib.worker.js +0 -1
  167. biolib/biolib-js/2-biolib.worker.js +0 -1
  168. biolib/biolib-js/3-biolib.worker.js +0 -1
  169. biolib/biolib-js/4-biolib.worker.js +0 -1
  170. biolib/biolib-js/5-biolib.worker.js +0 -1
  171. biolib/biolib-js/6-biolib.worker.js +0 -1
  172. biolib/biolib-js/index.html +0 -10
  173. biolib/biolib-js/main-biolib.js +0 -1
  174. biolib/biolib_api_client/biolib_account_api.py +0 -21
  175. biolib/biolib_api_client/biolib_large_file_system_api.py +0 -108
  176. biolib/biolib_binary_format/aes_encrypted_package.py +0 -42
  177. biolib/biolib_binary_format/module_output.py +0 -58
  178. biolib/biolib_binary_format/rsa_encrypted_aes_package.py +0 -57
  179. biolib/biolib_push.py +0 -114
  180. biolib/cli.py +0 -203
  181. biolib/cli_utils.py +0 -273
  182. biolib/compute_node/cloud_utils/enclave_parent_types.py +0 -7
  183. biolib/compute_node/enclave/__init__.py +0 -2
  184. biolib/compute_node/enclave/enclave_remote_hosts.py +0 -53
  185. biolib/compute_node/enclave/nitro_secure_module_utils.py +0 -64
  186. biolib/compute_node/job_worker/executors/base_executor.py +0 -18
  187. biolib/compute_node/job_worker/executors/pyppeteer_executor.py +0 -173
  188. biolib/compute_node/job_worker/executors/remote/__init__.py +0 -1
  189. biolib/compute_node/job_worker/executors/remote/nitro_enclave_utils.py +0 -81
  190. biolib/compute_node/job_worker/executors/remote/remote_executor.py +0 -51
  191. biolib/lfs.py +0 -196
  192. biolib/pyppeteer/.circleci/config.yml +0 -100
  193. biolib/pyppeteer/.coveragerc +0 -3
  194. biolib/pyppeteer/.gitignore +0 -89
  195. biolib/pyppeteer/.pre-commit-config.yaml +0 -28
  196. biolib/pyppeteer/CHANGES.md +0 -253
  197. biolib/pyppeteer/CONTRIBUTING.md +0 -26
  198. biolib/pyppeteer/LICENSE +0 -12
  199. biolib/pyppeteer/README.md +0 -137
  200. biolib/pyppeteer/docs/Makefile +0 -177
  201. biolib/pyppeteer/docs/_static/custom.css +0 -28
  202. biolib/pyppeteer/docs/_templates/layout.html +0 -10
  203. biolib/pyppeteer/docs/changes.md +0 -1
  204. biolib/pyppeteer/docs/conf.py +0 -299
  205. biolib/pyppeteer/docs/index.md +0 -21
  206. biolib/pyppeteer/docs/make.bat +0 -242
  207. biolib/pyppeteer/docs/reference.md +0 -211
  208. biolib/pyppeteer/docs/server.py +0 -60
  209. biolib/pyppeteer/poetry.lock +0 -1699
  210. biolib/pyppeteer/pyppeteer/__init__.py +0 -135
  211. biolib/pyppeteer/pyppeteer/accessibility.py +0 -286
  212. biolib/pyppeteer/pyppeteer/browser.py +0 -401
  213. biolib/pyppeteer/pyppeteer/browser_fetcher.py +0 -194
  214. biolib/pyppeteer/pyppeteer/command.py +0 -22
  215. biolib/pyppeteer/pyppeteer/connection/__init__.py +0 -242
  216. biolib/pyppeteer/pyppeteer/connection/cdpsession.py +0 -101
  217. biolib/pyppeteer/pyppeteer/coverage.py +0 -346
  218. biolib/pyppeteer/pyppeteer/device_descriptors.py +0 -787
  219. biolib/pyppeteer/pyppeteer/dialog.py +0 -79
  220. biolib/pyppeteer/pyppeteer/domworld.py +0 -597
  221. biolib/pyppeteer/pyppeteer/emulation_manager.py +0 -53
  222. biolib/pyppeteer/pyppeteer/errors.py +0 -48
  223. biolib/pyppeteer/pyppeteer/events.py +0 -63
  224. biolib/pyppeteer/pyppeteer/execution_context.py +0 -156
  225. biolib/pyppeteer/pyppeteer/frame/__init__.py +0 -299
  226. biolib/pyppeteer/pyppeteer/frame/frame_manager.py +0 -306
  227. biolib/pyppeteer/pyppeteer/helpers.py +0 -245
  228. biolib/pyppeteer/pyppeteer/input.py +0 -371
  229. biolib/pyppeteer/pyppeteer/jshandle.py +0 -598
  230. biolib/pyppeteer/pyppeteer/launcher.py +0 -683
  231. biolib/pyppeteer/pyppeteer/lifecycle_watcher.py +0 -169
  232. biolib/pyppeteer/pyppeteer/models/__init__.py +0 -103
  233. biolib/pyppeteer/pyppeteer/models/_protocol.py +0 -12460
  234. biolib/pyppeteer/pyppeteer/multimap.py +0 -82
  235. biolib/pyppeteer/pyppeteer/network_manager.py +0 -678
  236. biolib/pyppeteer/pyppeteer/options.py +0 -8
  237. biolib/pyppeteer/pyppeteer/page.py +0 -1728
  238. biolib/pyppeteer/pyppeteer/pipe_transport.py +0 -59
  239. biolib/pyppeteer/pyppeteer/target.py +0 -147
  240. biolib/pyppeteer/pyppeteer/task_queue.py +0 -24
  241. biolib/pyppeteer/pyppeteer/timeout_settings.py +0 -36
  242. biolib/pyppeteer/pyppeteer/tracing.py +0 -93
  243. biolib/pyppeteer/pyppeteer/us_keyboard_layout.py +0 -305
  244. biolib/pyppeteer/pyppeteer/util.py +0 -18
  245. biolib/pyppeteer/pyppeteer/websocket_transport.py +0 -47
  246. biolib/pyppeteer/pyppeteer/worker.py +0 -101
  247. biolib/pyppeteer/pyproject.toml +0 -97
  248. biolib/pyppeteer/spell.txt +0 -137
  249. biolib/pyppeteer/tox.ini +0 -72
  250. biolib/pyppeteer/utils/generate_protocol_types.py +0 -603
  251. biolib/start_cli.py +0 -7
  252. biolib/utils.py +0 -47
  253. biolib/validators/validate_app_version.py +0 -183
  254. biolib/validators/validate_argument.py +0 -134
  255. biolib/validators/validate_module.py +0 -323
  256. biolib/validators/validate_zip_file.py +0 -40
  257. biolib/validators/validator_utils.py +0 -103
  258. pybiolib-0.2.951.dist-info/LICENSE +0 -21
  259. pybiolib-0.2.951.dist-info/METADATA +0 -61
  260. pybiolib-0.2.951.dist-info/RECORD +0 -153
  261. pybiolib-0.2.951.dist-info/entry_points.txt +0 -3
  262. /LICENSE → /pybiolib-1.2.1890.dist-info/licenses/LICENSE +0 -0
@@ -1,157 +1,266 @@
1
- import time
2
- import tarfile
3
- import zipfile
4
- import os
5
1
  import io
2
+ import json
3
+ import os
4
+ import re
6
5
  import shlex
6
+ import subprocess
7
+ import tarfile
8
+ import tempfile
9
+ import time
10
+ import zipfile
11
+ from copy import copy
12
+ from datetime import datetime
7
13
 
8
- import docker # type: ignore
9
- from docker.errors import ImageNotFound, APIError # type: ignore
10
- from docker.models.containers import Container # type: ignore
14
+ import docker
15
+ import docker.types
16
+ from docker.errors import APIError, ImageNotFound
17
+ from docker.models.containers import Container
11
18
 
12
19
  from biolib import utils
13
- from biolib.biolib_binary_format import ModuleOutput, ModuleInput
20
+ from biolib._internal.runtime import RuntimeJobDataDict
21
+ from biolib.biolib_binary_format import ModuleInput, ModuleOutputV2
22
+ from biolib.biolib_binary_format.file_in_container import FileInContainer
14
23
  from biolib.biolib_docker_client import BiolibDockerClient
15
- from biolib.biolib_errors import DockerContainerNotFoundDuringExecutionException
16
- from biolib.biolib_logging import logger
24
+ from biolib.biolib_errors import BioLibError, DockerContainerNotFoundDuringExecutionException
25
+ from biolib.biolib_logging import logger, logger_no_user_data
17
26
  from biolib.compute_node import utils as compute_node_utils
27
+ from biolib.compute_node.cloud_utils import CloudUtils
18
28
  from biolib.compute_node.job_worker.docker_image_cache import DockerImageCache
19
- from biolib.compute_node.job_worker.executors.base_executor import BaseExecutor
20
- from biolib.compute_node.job_worker.executors.types import StatusUpdate, LocalExecutorOptions
29
+ from biolib.compute_node.job_worker.executors.docker_types import DockerDiffKind
30
+ from biolib.compute_node.job_worker.executors.types import LocalExecutorOptions, MetadataToSaveOutput, StatusUpdate
21
31
  from biolib.compute_node.job_worker.mappings import Mappings, path_without_first_folder
32
+ from biolib.compute_node.job_worker.utilization_reporter_thread import UtilizationReporterThread
22
33
  from biolib.compute_node.job_worker.utils import ComputeProcessException
23
34
  from biolib.compute_node.utils import SystemExceptionCodes
24
- from biolib.typing_utils import List, Any, Dict, Optional
25
- from biolib.utils import get_absolute_container_image_uri
35
+ from biolib.typing_utils import Dict, List, Optional
26
36
 
27
37
 
28
- class DockerExecutor(BaseExecutor):
29
-
30
- def __init__(self, options: LocalExecutorOptions):
31
- super().__init__(options)
32
-
33
- if self._options['root_job_id'] == utils.RUN_DEV_JOB_ID:
34
- self._image_uri = self._options['module']['image_uri']
35
- else:
36
- self._image_uri = get_absolute_container_image_uri(
37
- base_url=self._options['biolib_base_url'],
38
- relative_image_uri=self._options['module']['image_uri'],
39
- )
38
+ class DockerExecutor:
39
+ def __init__(self, options: LocalExecutorOptions) -> None:
40
+ self._options: LocalExecutorOptions = options
41
+ self._is_cleaning_up = False
40
42
 
43
+ self._absolute_image_uri = f'{utils.BIOLIB_SITE_HOSTNAME}/{self._options["module"]["image_uri"]}'
41
44
  self._send_system_exception = options['send_system_exception']
45
+ self._send_stdout_and_stderr = options['send_stdout_and_stderr']
46
+ self._random_docker_id = compute_node_utils.random_string(15)
47
+ total_memory_in_bytes = int(os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES'))
48
+ system_reserved_memory = int(total_memory_in_bytes * 0.1) + 500_000_000
49
+ self._available_memory_in_bytes = total_memory_in_bytes - system_reserved_memory
50
+ logger_no_user_data.info(f'Available memory for containers: {self._available_memory_in_bytes} bytes')
51
+
52
+ if utils.IS_RUNNING_IN_CLOUD:
53
+ self._compute_process_dir = os.getenv('BIOLIB_USER_DATA_PATH')
54
+ if not self._compute_process_dir:
55
+ raise Exception('Environment variable BIOLIB_USER_DATA_PATH is not set')
56
+ if not os.path.isdir(self._compute_process_dir):
57
+ raise Exception(f'User data directory {self._compute_process_dir} does not exist')
42
58
 
43
- if options['compute_node_info'] is not None:
44
- compute_node_public_id = options['compute_node_info']['public_id']
45
- compute_node_auth_token = options['compute_node_info']['auth_token']
46
- # Use "|" to separate the fields as this makes it easy for the ECR proxy to split
47
- ecr_proxy_auth_token = f'cloud-{compute_node_public_id}|{compute_node_auth_token}'
48
59
  else:
49
- ecr_proxy_auth_token = options['access_token']
60
+ self._compute_process_dir = os.path.dirname(os.path.realpath(__file__))
50
61
 
51
- job_id = self._options['job']['public_id']
52
- self._docker_auth_config = {'username': 'AWS', 'password': f'{ecr_proxy_auth_token},{job_id}'}
62
+ user_data_tar_dir = f'{self._compute_process_dir}/tars'
63
+ os.makedirs(user_data_tar_dir, exist_ok=True)
53
64
 
54
65
  self._docker_container: Optional[Container] = None
55
- self._random_docker_id = compute_node_utils.random_string(15)
56
- self._compute_process_dir = os.path.dirname(os.path.realpath(__file__))
57
- self._runtime_tar_path = f'{self._compute_process_dir}/tars/runtime_{self._random_docker_id}.tar'
58
- self._input_tar_path = f'{self._compute_process_dir}/tars/input_{self._random_docker_id}.tar'
59
-
60
- def execute_module(self, module_input_serialized: bytes) -> bytes:
61
- send_status_update = self._options['send_status_update']
62
- send_system_exception = self._options['send_system_exception']
63
-
64
- module_input = ModuleInput(module_input_serialized).deserialize()
65
-
66
- # TODO: fix these status updates such that they also make sense for run-dev
67
- send_status_update(StatusUpdate(progress=55, log_message='Pulling images...'))
68
-
69
- self._pull()
70
-
71
- send_status_update(StatusUpdate(progress=70, log_message='Computing...'))
72
- start_time = time.time()
73
-
74
- stdout, stderr, exit_code, mapped_output_files = self._execute_helper(module_input)
75
-
66
+ self._docker_api_client = BiolibDockerClient.get_docker_client().api
67
+ self._runtime_tar_path = f'{user_data_tar_dir}/runtime_{self._random_docker_id}.tar'
68
+ self._input_tar_path = f'{user_data_tar_dir}/input_{self._random_docker_id}.tar'
69
+
70
+ self._metadata_for_save_output_on_cancel: Optional[MetadataToSaveOutput] = None
71
+
72
+ if utils.IS_RUNNING_IN_CLOUD and not utils.BIOLIB_SECRETS_TMPFS_PATH:
73
+ error_message = 'Running in cloud but no TMPFS path has been set for secrets'
74
+ logger_no_user_data.error(error_message)
75
+ raise BioLibError(error_message)
76
+
77
+ # If BIOLIB_SECRETS_TMPFS_PATH is set create the temporary directory there
78
+ self._tmp_secrets_dir = tempfile.TemporaryDirectory(dir=utils.BIOLIB_SECRETS_TMPFS_PATH or None)
79
+ self._tmp_client_secrets_dir = tempfile.TemporaryDirectory(dir=utils.BIOLIB_SECRETS_TMPFS_PATH or None)
80
+ os.chmod(self._tmp_secrets_dir.name, 0o755)
81
+ os.chmod(self._tmp_client_secrets_dir.name, 0o755)
82
+
83
+ @property
84
+ def _container(self) -> Container:
85
+ if self._docker_container is None:
86
+ raise Exception('Docker container was None')
87
+ return self._docker_container
88
+
89
+ def execute_module(self) -> None:
76
90
  try:
77
- module_output_serialized: bytes = ModuleOutput().serialize(stdout, stderr, exit_code, mapped_output_files)
78
- logger.debug(f'Compute time: {time.time() - start_time}')
79
- return module_output_serialized
91
+ job_uuid = self._options['job']['public_id']
92
+ send_status_update = self._options['send_status_update']
93
+ logger_no_user_data.debug(f'Reading module input of {job_uuid}.')
94
+ with open(self._options['module_input_path'], 'rb') as fp:
95
+ module_input_tmp = ModuleInput(fp.read())
96
+ logger_no_user_data.debug(f'Deserialing module input of {job_uuid}...')
97
+ module_input = module_input_tmp.deserialize()
98
+
99
+ send_status_update(StatusUpdate(progress=55, log_message='Pulling images...'))
100
+ logger_no_user_data.debug(f'Pulling image for {job_uuid}.')
101
+ self._pull()
102
+
103
+ send_status_update(StatusUpdate(progress=70, log_message='Computing...'))
104
+ start_time = time.time()
80
105
 
81
- except Exception as exception:
82
- raise ComputeProcessException(
83
- exception,
84
- SystemExceptionCodes.FAILED_TO_SERIALIZE_AND_SEND_MODULE_OUTPUT.value,
85
- send_system_exception
86
- ) from exception
106
+ logger_no_user_data.debug(f'Starting execution of {job_uuid}.')
107
+ try:
108
+ self._execute_helper(module_input)
109
+ except docker.errors.NotFound as docker_error:
110
+ raise DockerContainerNotFoundDuringExecutionException from docker_error
111
+ except Exception as exception:
112
+ raise ComputeProcessException(
113
+ exception,
114
+ SystemExceptionCodes.FAILED_TO_RUN_COMPUTE_CONTAINER.value,
115
+ self._send_system_exception,
116
+ ) from exception
117
+ logger_no_user_data.debug(f'Completed execution of {job_uuid}.')
118
+ logger_no_user_data.debug(f'Compute time: {time.time() - start_time}')
87
119
  finally:
88
120
  try:
89
121
  self.cleanup()
90
122
  except Exception: # pylint: disable=broad-except
91
- logger.error('DockerExecutor failed to clean up container')
92
-
93
- def _pull(self):
94
- try:
95
- start_time = time.time()
96
- if utils.IS_RUNNING_IN_CLOUD:
97
- DockerImageCache().get(
98
- image_uri=self._image_uri,
99
- estimated_image_size_bytes=self._options['module']['estimated_image_size_bytes'],
100
- pull_auth_config=self._docker_auth_config
123
+ logger_no_user_data.error('DockerExecutor failed to clean up container')
124
+
125
+ def _pull(self) -> None:
126
+ retries = 3
127
+ last_error: Optional[Exception] = None
128
+ estimated_image_size_bytes = self._options['module']['estimated_image_size_bytes']
129
+ assert estimated_image_size_bytes is not None, 'No estimated image size'
130
+
131
+ for retry_count in range(retries + 1):
132
+ if retry_count > 0:
133
+ logger_no_user_data.debug(f'Retrying Docker image pull of "{self._absolute_image_uri}"')
134
+ time.sleep(5 * retry_count)
135
+ try:
136
+ start_time = time.time()
137
+ if utils.IS_RUNNING_IN_CLOUD:
138
+ DockerImageCache().get(
139
+ image_uri=self._absolute_image_uri,
140
+ estimated_image_size_bytes=estimated_image_size_bytes,
141
+ job_id=self._options['job']['public_id'],
142
+ )
143
+ else:
144
+ docker_client = BiolibDockerClient.get_docker_client()
145
+ try:
146
+ docker_client.images.get(self._absolute_image_uri)
147
+ except ImageNotFound:
148
+ job_uuid = self._options['job']['public_id']
149
+ docker_client.images.pull(
150
+ self._absolute_image_uri,
151
+ auth_config={'username': 'biolib', 'password': f',{job_uuid}'},
152
+ )
153
+
154
+ logger_no_user_data.debug(f'Pulled image in: {time.time() - start_time}')
155
+ return
156
+ except Exception as error:
157
+ logger_no_user_data.warning(
158
+ f'Pull of Docker image "{self._absolute_image_uri}" returned error: {error}'
101
159
  )
102
- else:
103
- docker_client = BiolibDockerClient.get_docker_client()
104
- try:
105
- docker_client.images.get(self._image_uri)
106
- except ImageNotFound:
107
- docker_client.images.pull(self._image_uri, auth_config=self._docker_auth_config)
108
- logger.debug(f'Pulled image in: {time.time() - start_time}')
109
-
110
- except Exception as exception:
111
- raise ComputeProcessException(
112
- exception,
113
- SystemExceptionCodes.FAILED_TO_PULL_DOCKER_IMAGE.value,
114
- self._send_system_exception,
115
- may_contain_user_data=False
116
- ) from exception
117
-
118
- def _execute_helper(self, module_input):
160
+ last_error = error
161
+
162
+ raise ComputeProcessException(
163
+ last_error or Exception('Retries exceeded: failed to pull Docker image'),
164
+ SystemExceptionCodes.FAILED_TO_PULL_DOCKER_IMAGE.value,
165
+ self._send_system_exception,
166
+ may_contain_user_data=False,
167
+ )
168
+
169
+ def _execute_helper(self, module_input) -> None:
170
+ job_uuid = self._options['job']['public_id']
171
+ logger_no_user_data.debug(f'Initializing container for {job_uuid}.')
119
172
  self._initialize_docker_container(module_input)
120
173
 
174
+ if utils.IS_RUNNING_IN_CLOUD:
175
+ logger_no_user_data.debug(f'Job "{job_uuid}" starting utilization metrics reporter thread...')
176
+ config = CloudUtils.get_webserver_config()
177
+ node_auth_token = config['compute_node_info']['auth_token'] # pylint: disable=unsubscriptable-object
178
+ cloud_job = self._options['cloud_job']
179
+ include_gpu_stats = False
180
+ if cloud_job:
181
+ include_gpu_stats = cloud_job.get('reserved_gpu_count', 0) > 0
182
+ UtilizationReporterThread(
183
+ container=self._container,
184
+ job_uuid=job_uuid,
185
+ compute_node_auth_token=node_auth_token,
186
+ include_gpu_stats=include_gpu_stats,
187
+ ).start()
188
+
121
189
  if self._options['runtime_zip_bytes']:
122
190
  self._map_and_copy_runtime_files_to_container(self._options['runtime_zip_bytes'], module_input['arguments'])
123
191
 
192
+ logger_no_user_data.debug(f'_map_and_copy_input_files_to_container for {job_uuid}.')
124
193
  self._map_and_copy_input_files_to_container(module_input['files'], module_input['arguments'])
125
194
 
126
- try:
127
- docker_api_client = BiolibDockerClient.get_docker_client().api
128
- logger.debug('Starting Docker container')
129
- self._docker_container.start()
195
+ logger_no_user_data.debug(f'Attaching Docker container for {job_uuid}')
130
196
 
131
- exit_code = docker_api_client.wait(self._docker_container.id)['StatusCode']
132
- logger.debug(f'Docker container exited with code {exit_code}')
197
+ stdout_and_stderr_stream = self._docker_api_client.attach(
198
+ container=self._container.id,
199
+ stderr=True,
200
+ stdout=True,
201
+ stream=True,
202
+ )
133
203
 
134
- stdout = docker_api_client.logs(self._docker_container.id, stdout=True, stderr=False)
135
- stderr = docker_api_client.logs(self._docker_container.id, stdout=False, stderr=True)
204
+ logger_no_user_data.debug(f'Starting Docker container for {job_uuid}')
205
+ startup_error_string: Optional[str] = None
206
+ try:
207
+ self._container.start()
208
+ except APIError:
209
+ logger_no_user_data.debug(f'Warning: Job "{job_uuid}" failed to start container')
210
+ self._container.reload()
211
+ startup_error_string = self._container.attrs['State'].get('Error')
212
+ logger.debug(f'Warning: Job "{job_uuid}" failed to start container. Hit error: {startup_error_string}')
213
+ # even though the container start failed we should still be able to call logs() and wait() on it, so we pass
214
+
215
+ self._metadata_for_save_output_on_cancel = MetadataToSaveOutput(
216
+ arguments=module_input['arguments'],
217
+ startup_error_string=startup_error_string,
218
+ )
219
+
220
+ if self._options['job']['app_version'].get('stdout_render_type') != 'markdown':
221
+ logger_no_user_data.debug(f'Streaming stdout for {job_uuid}')
222
+ for stdout_and_stderr in stdout_and_stderr_stream:
223
+ # Default messages to empty bytestring instead of None
224
+ stdout_and_stderr = stdout_and_stderr if stdout_and_stderr is not None else b''
225
+
226
+ self._send_stdout_and_stderr(stdout_and_stderr)
227
+
228
+ logger_no_user_data.debug(f'Waiting on docker for {job_uuid}')
229
+ try:
230
+ docker_result = self._docker_api_client.wait(self._container.id)
231
+ except docker.errors.NotFound as error:
232
+ if self._is_cleaning_up:
233
+ return
234
+ else:
235
+ raise error
136
236
 
137
- if utils.BIOLIB_IS_RUNNING_IN_ENCLAVE:
138
- stderr = stderr.replace(
139
- b'OpenBLAS WARNING - could not determine the L2 cache size on this system, assuming 256k\n',
140
- b'',
141
- )
237
+ logger_no_user_data.debug(f'Got result from docker for {job_uuid}')
238
+ exit_code = docker_result['StatusCode']
239
+ # 137 is the error code from linux OOM killer (Should catch 90% of OOM errors)
240
+ if exit_code == 137:
241
+ raise ComputeProcessException(
242
+ MemoryError(),
243
+ SystemExceptionCodes.OUT_OF_MEMORY.value,
244
+ self._send_system_exception,
245
+ )
142
246
 
143
- mapped_output_files = self._get_output_files(arguments=module_input['arguments'])
144
- return stdout, stderr, exit_code, mapped_output_files
247
+ logger_no_user_data.debug(f'Docker container exited with code {exit_code} for {job_uuid}')
248
+ self._save_module_output_from_container(exit_code, self._metadata_for_save_output_on_cancel)
145
249
 
146
- except docker.errors.NotFound as docker_error:
147
- raise DockerContainerNotFoundDuringExecutionException from docker_error
250
+ def _save_module_output_from_container(self, exit_code: int, metadata: MetadataToSaveOutput) -> None:
251
+ full_stdout = self._docker_api_client.logs(self._container.id, stdout=True, stderr=False)
252
+ full_stderr = self._docker_api_client.logs(self._container.id, stdout=False, stderr=True)
148
253
 
149
- except Exception as exception:
150
- raise ComputeProcessException(
151
- exception,
152
- SystemExceptionCodes.FAILED_TO_RUN_COMPUTE_CONTAINER.value,
153
- self._send_system_exception
154
- ) from exception
254
+ if metadata['startup_error_string']:
255
+ full_stderr = full_stderr + metadata['startup_error_string'].encode()
256
+
257
+ self._write_module_output_to_file(
258
+ arguments=metadata['arguments'],
259
+ exit_code=exit_code,
260
+ module_output_path=self._options['module_output_path'],
261
+ stderr=full_stderr,
262
+ stdout=full_stdout,
263
+ )
155
264
 
156
265
  def cleanup(self):
157
266
  # Don't clean up if already in the process of doing so, or done doing so
@@ -160,45 +269,195 @@ class DockerExecutor(BaseExecutor):
160
269
  else:
161
270
  self._is_cleaning_up = True
162
271
 
272
+ if self._metadata_for_save_output_on_cancel is not None:
273
+ try:
274
+ logger_no_user_data.debug('Attempting to save results')
275
+ self._docker_container.stop()
276
+ self._docker_container.reload()
277
+ logger_no_user_data.debug(f'Container state {self._docker_container.status}')
278
+ self._save_module_output_from_container(
279
+ exit_code=self._docker_container.attrs['State']['ExitCode'],
280
+ metadata=self._metadata_for_save_output_on_cancel,
281
+ )
282
+ logger_no_user_data.debug('Saved results')
283
+ except BaseException as error:
284
+ logger_no_user_data.error(f'Failed to save results on cancel with error: {error}')
285
+ else:
286
+ logger_no_user_data.debug('Missing metadata, cannot save results')
287
+
163
288
  tar_time = time.time()
164
289
  for path_to_delete in [self._input_tar_path, self._runtime_tar_path]:
165
290
  if os.path.exists(path_to_delete):
166
291
  os.remove(path_to_delete)
167
- logger.debug(f"Deleted tars in: {time.time() - tar_time}")
292
+ logger_no_user_data.debug(f'Deleted tars in: {time.time() - tar_time}')
168
293
 
169
294
  container_time = time.time()
170
295
  if self._docker_container:
171
296
  self._docker_container.remove(force=True)
172
297
 
173
- logger.debug(f"Deleted compute container in: {time.time() - container_time}")
298
+ if utils.IS_RUNNING_IN_CLOUD:
299
+ DockerImageCache().detach_job(image_uri=self._absolute_image_uri, job_id=self._options['job']['public_id'])
300
+
301
+ logger_no_user_data.debug(f'Deleted compute container in: {time.time() - container_time}')
302
+ self._tmp_secrets_dir.cleanup()
303
+ self._tmp_client_secrets_dir.cleanup()
174
304
 
175
305
  # TODO: type this method
176
306
  def _initialize_docker_container(self, module_input):
177
307
  try:
308
+ job_uuid = self._options['job']['public_id']
309
+ logger_no_user_data.debug(f'Job "{job_uuid}" initializing Docker container...')
178
310
  module = self._options['module']
179
- logger.debug(f"Initializing docker container with command: {module['command']}")
311
+ logger.debug(f'Initializing docker container with command: {module["command"]}')
312
+ docker_client = BiolibDockerClient.get_docker_client()
180
313
 
181
314
  docker_volume_mounts = [lfs.docker_mount for lfs in self._options['large_file_systems'].values()]
182
315
 
183
316
  internal_network = self._options['internal_network']
184
317
  extra_hosts: Dict[str, str] = {}
185
- for proxy in self._options['remote_host_proxies']:
186
- extra_hosts[proxy.hostname] = proxy.get_ip_address_on_network(internal_network)
187
-
188
- self._docker_container = BiolibDockerClient.get_docker_client().containers.create(
189
- command=shlex.split(module['command']) + module_input['arguments'],
190
- extra_hosts=extra_hosts,
191
- image=self._image_uri,
192
- mounts=docker_volume_mounts,
193
- network=internal_network.name if internal_network else None,
194
- working_dir=module['working_directory'],
318
+
319
+ biolib_system_secret = RuntimeJobDataDict(
320
+ version='1.0.0',
321
+ job_requested_machine=self._options['job']['requested_machine'],
322
+ job_requested_machine_spot=self._options['job'].get('requested_machine_spot', False),
323
+ job_uuid=self._options['job']['public_id'],
324
+ job_auth_token=self._options['job']['auth_token'],
325
+ app_uri=self._options['job']['app_uri'],
326
+ is_environment_biolib_cloud=bool(utils.IS_RUNNING_IN_CLOUD),
327
+ job_reserved_machines=self._options['job']['reserved_machines'],
328
+ )
329
+ docker_volume_mounts.append(
330
+ self._create_secrets_mount(
331
+ source_dir=self._tmp_secrets_dir.name + '/',
332
+ target_dir='/biolib/secrets/',
333
+ secrets=dict(
334
+ **module.get('secrets', {}),
335
+ biolib_system_secret=json.dumps(biolib_system_secret, indent=4),
336
+ ),
337
+ )
195
338
  )
196
- logger.debug('Finished initializing docker container')
339
+ docker_volume_mounts.append(
340
+ self._create_secrets_mount(
341
+ source_dir=self._tmp_client_secrets_dir.name + '/',
342
+ target_dir='/biolib/temporary-client-secrets/',
343
+ secrets=self._options['job'].get('temporary_client_secrets', {}),
344
+ )
345
+ )
346
+
347
+ environment_vars = {}
348
+
349
+ # Include secrets and job info as env vars for app versions created before 20/11/2022
350
+ app_version_created_at = datetime.strptime(
351
+ self._options['job']['app_version']['created_at'],
352
+ '%Y-%m-%dT%H:%M:%S.%fZ',
353
+ )
354
+ if app_version_created_at < datetime(2022, 11, 30, 0, 0):
355
+ environment_vars = module.get('secrets', {})
356
+ environment_vars.update(
357
+ {
358
+ 'BIOLIB_JOB_UUID': self._options['job']['public_id'],
359
+ 'BIOLIB_JOB_AUTH_TOKEN': self._options['job']['auth_token'],
360
+ }
361
+ )
362
+
363
+ if utils.IS_RUNNING_IN_CLOUD and self._options['cloud_job']:
364
+ environment_vars.update(
365
+ {
366
+ 'BIOLIB_JOB_MAX_RUNTIME_IN_SECONDS': self._options['cloud_job']['max_runtime_in_seconds'],
367
+ }
368
+ )
369
+
370
+ logger_no_user_data.debug(f'Job "{job_uuid}" initializing Docker container. Getting IPs for proxies...')
371
+
372
+ networks_to_connect = []
373
+ for proxy in self._options['remote_host_proxies']:
374
+ if proxy.is_app_caller_proxy:
375
+ proxy_ip = proxy.get_ip_address_on_network(internal_network)
376
+ logger_no_user_data.debug('Found app caller proxy, setting both base URLs in compute container')
377
+ environment_vars.update(
378
+ {
379
+ 'BIOLIB_BASE_URL': f'http://{proxy_ip}',
380
+ 'BIOLIB_CLOUD_BASE_URL': f'http://{proxy_ip}',
381
+ # This should be removed eventually, but will break apps calling apps on older versions
382
+ 'BIOLIB_CLOUD_RESULTS_BASE_URL': f'http://{proxy_ip}',
383
+ 'BIOLIB_CLOUD_JOB_STORAGE_BASE_URL': f'http://{proxy_ip}',
384
+ # Inform container if we are targeting public biolib as we change the BIOLIB_BASE_URL
385
+ 'BIOLIB_ENVIRONMENT_IS_PUBLIC_BIOLIB': bool(utils.BASE_URL_IS_PUBLIC_BIOLIB),
386
+ }
387
+ )
388
+ else:
389
+ extra_hosts.update(proxy.get_hostname_to_ip_mapping())
390
+
391
+ for network in proxy.get_remote_host_networks():
392
+ if network != internal_network:
393
+ networks_to_connect.append(network)
394
+
395
+ logger_no_user_data.debug(f'Job "{job_uuid}" initializing Docker container. Constructing container args...')
396
+ create_container_args = {
397
+ 'environment': environment_vars,
398
+ 'extra_hosts': extra_hosts,
399
+ 'image': self._absolute_image_uri,
400
+ 'mounts': docker_volume_mounts,
401
+ 'network': internal_network.name,
402
+ 'working_dir': module['working_directory'],
403
+ 'networking_config': {
404
+ internal_network.name: docker_client.api.create_endpoint_config(aliases=['main'])
405
+ },
406
+ }
407
+
408
+ if self._options['job'].get('arguments_override_command'):
409
+ # In this case, arguments contains a user specified command to run in the app
410
+ create_container_args.update({'command': module_input['arguments'], 'entrypoint': ''})
411
+
412
+ else:
413
+ create_container_args.update({'command': shlex.split(module['command']) + module_input['arguments']})
414
+
415
+ app_version = self._options['job']['app_version']
416
+ if app_version.get('main_output_file') or app_version.get('stdout_render_type') == 'text':
417
+ create_container_args['tty'] = True
418
+
419
+ if utils.IS_RUNNING_IN_CLOUD:
420
+ cloud_job = self._options['cloud_job']
421
+ container_memory_limit_in_bytes = min(
422
+ cloud_job['reserved_memory_in_bytes'], self._available_memory_in_bytes
423
+ )
424
+ create_container_args['mem_limit'] = f'{container_memory_limit_in_bytes}b'
425
+ logger_no_user_data.debug(
426
+ 'Setting container memory limit to '
427
+ f'{container_memory_limit_in_bytes} bytes '
428
+ f'(requested: {cloud_job["reserved_memory_in_bytes"]}, '
429
+ f'available: {self._available_memory_in_bytes})'
430
+ )
431
+ create_container_args['nano_cpus'] = cloud_job['reserved_cpu_in_nano_shares']
432
+ create_container_args['pids_limit'] = 10_000
433
+
434
+ biolib_identity_user_email: Optional[str] = cloud_job.get('biolib_identity_user_email')
435
+ if biolib_identity_user_email:
436
+ create_container_args['environment'].update(
437
+ {'BIOLIB_IDENTITY_USER_EMAIL': biolib_identity_user_email}
438
+ )
439
+
440
+ docker_runtime = os.getenv('BIOLIB_DOCKER_RUNTIME')
441
+ if docker_runtime is not None:
442
+ create_container_args['runtime'] = docker_runtime
443
+
444
+ logger_no_user_data.debug(f'Job "{job_uuid}" initializing Docker container. Creating container...')
445
+ self._docker_container = docker_client.containers.create(**create_container_args)
446
+
447
+ if networks_to_connect:
448
+ network_connection_start = time.time()
449
+ for network in networks_to_connect:
450
+ network.connect(self._docker_container.id)
451
+ logger_no_user_data.debug(f'Connected app container to network {network.name}')
452
+ network_connection_time = time.time() - network_connection_start
453
+ logger_no_user_data.debug(
454
+ f'Connected app container to {len(networks_to_connect)} networks in {network_connection_time:.2f}s'
455
+ )
456
+
457
+ logger_no_user_data.debug(f'Job "{job_uuid}" finished initializing Docker container.')
197
458
  except Exception as exception:
198
459
  raise ComputeProcessException(
199
- exception,
200
- SystemExceptionCodes.FAILED_TO_START_COMPUTE_CONTAINER.value,
201
- self._send_system_exception
460
+ exception, SystemExceptionCodes.FAILED_TO_START_COMPUTE_CONTAINER.value, self._send_system_exception
202
461
  ) from exception
203
462
 
204
463
  def _add_file_to_tar(self, tar, current_path, mapped_path, data):
@@ -265,7 +524,7 @@ class DockerExecutor(BaseExecutor):
265
524
  raise ComputeProcessException(
266
525
  exception,
267
526
  SystemExceptionCodes.FAILED_TO_COPY_INPUT_FILES_TO_COMPUTE_CONTAINER.value,
268
- self._send_system_exception
527
+ self._send_system_exception,
269
528
  ) from exception
270
529
 
271
530
  def _map_and_copy_runtime_files_to_container(self, runtime_zip_data, arguments: List[str], remove_root_folder=True):
@@ -280,80 +539,154 @@ class DockerExecutor(BaseExecutor):
280
539
  raise ComputeProcessException(
281
540
  exception,
282
541
  SystemExceptionCodes.FAILED_TO_COPY_RUNTIME_FILES_TO_COMPUTE_CONTAINER.value,
283
- self._send_system_exception
542
+ self._send_system_exception,
284
543
  ) from exception
285
544
 
286
- def _get_output_files(self, arguments: List[str]):
287
- module = self._options['module']
545
+ def _write_module_output_to_file(
546
+ self,
547
+ arguments: List[str],
548
+ exit_code: int,
549
+ module_output_path: str,
550
+ stderr: bytes,
551
+ stdout: bytes,
552
+ ) -> None:
553
+ mapped_files: List[FileInContainer] = []
288
554
  try:
289
- if self._docker_container is None:
290
- raise Exception('Docker container was None')
555
+ mappings = Mappings(mappings_list=self._options['module']['output_files_mappings'], arguments=arguments)
556
+ changed_files: List[FileInContainer] = self._get_changed_files_in_docker_container()
557
+
558
+ for file in changed_files:
559
+ if file.is_file():
560
+ for mapped_path in mappings.get_mappings_for_path(file.path):
561
+ mapped_output_file = copy(file)
562
+ mapped_output_file.path = mapped_path
563
+ mapped_files.append(mapped_output_file)
291
564
 
292
- docker_api_client = BiolibDockerClient.get_docker_client().api
293
-
294
- # TODO: fix typing
295
- input_tar: Any = None
296
- if os.path.exists(self._input_tar_path):
297
- input_tar = tarfile.open(self._input_tar_path)
298
- input_tar_filelist = input_tar.getnames()
299
-
300
- # TODO: fix typing
301
- runtime_tar: Any = None
302
- if os.path.exists(self._runtime_tar_path):
303
- runtime_tar = tarfile.open(self._runtime_tar_path)
304
- runtime_tar_filelist = runtime_tar.getnames()
305
-
306
- mapped_output_files = {}
307
- for mapping in module['output_files_mappings']:
308
- try:
309
- tar_bytes_generator, _ = docker_api_client.get_archive(
310
- self._docker_container.id, mapping['from_path'])
311
- except APIError:
312
- logger.warning(f'Could not get output from path {mapping["from_path"]}')
313
- continue
314
-
315
- tar_bytes_obj = io.BytesIO()
316
- for chunk in tar_bytes_generator:
317
- tar_bytes_obj.write(chunk)
318
-
319
- tar = tarfile.open(fileobj=io.BytesIO(tar_bytes_obj.getvalue()))
320
- for file in tar.getmembers():
321
- file_obj = tar.extractfile(file)
322
-
323
- # Skip empty dirs
324
- if not file_obj:
325
- continue
326
- file_data = file_obj.read()
327
-
328
- # Remove parent dir from tar file name and prepend from_path.
329
- # Except if from_path is root '/', that works out of the box
330
- if mapping['from_path'].endswith('/') and mapping['from_path'] != '/':
331
- file_name = mapping['from_path'] + path_without_first_folder(file.name)
332
-
333
- # When getting a file use the from_path.
334
- # This is due to directory info (absolute path) being lost when using get_archive on files
335
- else:
336
- file_name = mapping['from_path']
337
-
338
- # Filter out unchanged input files
339
- if input_tar and file_name in input_tar_filelist and \
340
- input_tar.extractfile(file_name).read() == file_data:
341
- continue
342
-
343
- # Filter out unchanged source files if provided
344
- if runtime_tar and file_name in runtime_tar_filelist and runtime_tar.extractfile(
345
- file_name).read() == file_data:
346
- continue
347
-
348
- mapped_file_names = Mappings([mapping], arguments).get_mappings_for_path(file_name)
349
- for mapped_file_name in mapped_file_names:
350
- mapped_output_files[mapped_file_name] = file_data
565
+ except Exception as exception:
566
+ raise ComputeProcessException(
567
+ exception,
568
+ SystemExceptionCodes.FAILED_TO_RETRIEVE_AND_MAP_OUTPUT_FILES.value,
569
+ self._send_system_exception,
570
+ ) from exception
351
571
 
572
+ try:
573
+ ModuleOutputV2.write_to_file(
574
+ exit_code=exit_code,
575
+ files=mapped_files,
576
+ output_file_path=module_output_path,
577
+ stderr=stderr,
578
+ stdout=stdout,
579
+ )
352
580
  except Exception as exception:
581
+ logger.error('Hit Error 23')
582
+ logger.exception('Error')
583
+ logger.error(str(exception))
584
+ time.sleep(3)
353
585
  raise ComputeProcessException(
354
586
  exception,
355
- SystemExceptionCodes.FAILED_TO_RUN_COMPUTE_CONTAINER.value,
356
- self._send_system_exception
587
+ SystemExceptionCodes.FAILED_TO_SERIALIZE_AND_SEND_MODULE_OUTPUT.value,
588
+ self._send_system_exception,
357
589
  ) from exception
358
590
 
359
- return mapped_output_files
591
+ def _get_container_upper_dir_path(self) -> Optional[str]:
592
+ data = self._container.attrs['GraphDriver']['Data']
593
+ upper_dir: Optional[str] = data['UpperDir'] if data else None
594
+
595
+ if not upper_dir and utils.IS_RUNNING_IN_CLOUD:
596
+ # Get upperdir from containerd ctr CLI
597
+ result = subprocess.run(
598
+ args=[
599
+ 'ctr',
600
+ '--namespace',
601
+ 'moby',
602
+ 'snapshots',
603
+ '--snapshotter',
604
+ 'nydus',
605
+ 'mounts',
606
+ '/some_arbitrary_path',
607
+ str(self._container.id),
608
+ ],
609
+ check=False,
610
+ capture_output=True,
611
+ )
612
+ if result.returncode == 0:
613
+ match = re.search(r'upperdir=([^,]+)', result.stdout.decode('utf-8'))
614
+ upper_dir = match.group(1) if match else None
615
+
616
+ if upper_dir and os.path.exists(upper_dir):
617
+ return upper_dir
618
+
619
+ return None
620
+
621
+ def _get_changed_files_in_docker_container(self) -> List[FileInContainer]:
622
+ from_mappings = [mapping['from_path'] for mapping in self._options['module']['output_files_mappings']]
623
+ overlay_upper_dir_path = self._get_container_upper_dir_path()
624
+
625
+ if not overlay_upper_dir_path:
626
+ logger_no_user_data.debug(
627
+ 'Docker UpperDir not available. Falling back to container.get_archive() for file extraction'
628
+ )
629
+ post_run_diff = self._container.diff()
630
+ run_diff_paths: List[str] = [
631
+ obj['Path']
632
+ for obj in post_run_diff
633
+ if obj['Kind'] in (DockerDiffKind.CHANGED.value, DockerDiffKind.ADDED.value)
634
+ ]
635
+ else:
636
+ logger_no_user_data.debug(f'overlay_upper_dir_path={overlay_upper_dir_path}')
637
+ # Recursively find all files in overlay_upper_dir_path
638
+ run_diff_paths = []
639
+ for root, _, files in os.walk(overlay_upper_dir_path):
640
+ # Convert absolute paths to container paths
641
+ rel_path = os.path.relpath(root, overlay_upper_dir_path)
642
+ if rel_path == '.':
643
+ # Handle the root directory case
644
+ for file in files:
645
+ run_diff_paths.append(f'/{file}')
646
+ else:
647
+ # Handle subdirectories
648
+ for file in files:
649
+ run_diff_paths.append(f'/{rel_path}/{file}')
650
+
651
+ known_directories = set()
652
+ for path in run_diff_paths:
653
+ parent_folders = path.split('/')[:-1]
654
+ for idx in range(len(parent_folders)):
655
+ if idx == 0:
656
+ continue # always skip root
657
+
658
+ folder = '/' + '/'.join(parent_folders[1 : idx + 1])
659
+ known_directories.add(folder)
660
+
661
+ def path_is_included_in_from_mappings(path: str) -> bool:
662
+ for from_mapping in from_mappings:
663
+ if path.startswith(from_mapping):
664
+ return True
665
+ return False
666
+
667
+ files_and_empty_dirs: List[FileInContainer] = []
668
+ for path in run_diff_paths:
669
+ if path not in known_directories and path_is_included_in_from_mappings(path):
670
+ files_and_empty_dirs.append(
671
+ FileInContainer(
672
+ container=self._container,
673
+ overlay_upper_dir_path=overlay_upper_dir_path,
674
+ path_in_container=path,
675
+ )
676
+ )
677
+
678
+ return files_and_empty_dirs
679
+
680
+ def _create_secrets_mount(self, source_dir: str, target_dir: str, secrets: Dict[str, str]) -> docker.types.Mount:
681
+ assert source_dir.startswith('/') and source_dir.endswith('/'), 'source_dir must start and end with slash'
682
+ assert target_dir.startswith('/') and target_dir.endswith('/'), 'target_dir must start and end with slash'
683
+
684
+ job_uuid = self._options['job']['public_id']
685
+ for key, value in secrets.items():
686
+ if re.match(r'^[a-zA-Z0-9-_]+$', key):
687
+ with open(f'{source_dir}{key}', 'w') as secret_file:
688
+ secret_file.write(value)
689
+ else:
690
+ logger_no_user_data.warning(f'Job {job_uuid} uses secret with a key not matching validation regex')
691
+
692
+ return docker.types.Mount(read_only=True, source=source_dir, target=target_dir, type='bind')