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.
- biolib/__init__.py +357 -11
- biolib/_data_record/data_record.py +380 -0
- biolib/_index/__init__.py +0 -0
- biolib/_index/index.py +55 -0
- biolib/_index/query_result.py +103 -0
- biolib/_internal/__init__.py +0 -0
- biolib/_internal/add_copilot_prompts.py +58 -0
- biolib/_internal/add_gui_files.py +81 -0
- biolib/_internal/data_record/__init__.py +1 -0
- biolib/_internal/data_record/data_record.py +85 -0
- biolib/_internal/data_record/push_data.py +116 -0
- biolib/_internal/data_record/remote_storage_endpoint.py +43 -0
- biolib/_internal/errors.py +5 -0
- biolib/_internal/file_utils.py +125 -0
- biolib/_internal/fuse_mount/__init__.py +1 -0
- biolib/_internal/fuse_mount/experiment_fuse_mount.py +209 -0
- biolib/_internal/http_client.py +159 -0
- biolib/_internal/lfs/__init__.py +1 -0
- biolib/_internal/lfs/cache.py +51 -0
- biolib/_internal/libs/__init__.py +1 -0
- biolib/_internal/libs/fusepy/__init__.py +1257 -0
- biolib/_internal/push_application.py +488 -0
- biolib/_internal/runtime.py +22 -0
- biolib/_internal/string_utils.py +13 -0
- biolib/_internal/templates/__init__.py +1 -0
- biolib/_internal/templates/copilot_template/.github/instructions/general-app-knowledge.instructions.md +10 -0
- biolib/_internal/templates/copilot_template/.github/instructions/style-general.instructions.md +20 -0
- biolib/_internal/templates/copilot_template/.github/instructions/style-python.instructions.md +16 -0
- biolib/_internal/templates/copilot_template/.github/instructions/style-react-ts.instructions.md +47 -0
- biolib/_internal/templates/copilot_template/.github/prompts/biolib_app_inputs.prompt.md +11 -0
- biolib/_internal/templates/copilot_template/.github/prompts/biolib_onboard_repo.prompt.md +19 -0
- biolib/_internal/templates/copilot_template/.github/prompts/biolib_run_apps.prompt.md +12 -0
- biolib/_internal/templates/dashboard_template/.biolib/config.yml +5 -0
- biolib/_internal/templates/github_workflow_template/.github/workflows/biolib.yml +21 -0
- biolib/_internal/templates/gitignore_template/.gitignore +10 -0
- biolib/_internal/templates/gui_template/.yarnrc.yml +1 -0
- biolib/_internal/templates/gui_template/App.tsx +53 -0
- biolib/_internal/templates/gui_template/Dockerfile +27 -0
- biolib/_internal/templates/gui_template/biolib-sdk.ts +82 -0
- biolib/_internal/templates/gui_template/dev-data/output.json +7 -0
- biolib/_internal/templates/gui_template/index.css +5 -0
- biolib/_internal/templates/gui_template/index.html +13 -0
- biolib/_internal/templates/gui_template/index.tsx +10 -0
- biolib/_internal/templates/gui_template/package.json +27 -0
- biolib/_internal/templates/gui_template/tsconfig.json +24 -0
- biolib/_internal/templates/gui_template/vite-plugin-dev-data.ts +50 -0
- biolib/_internal/templates/gui_template/vite.config.mts +10 -0
- biolib/_internal/templates/init_template/.biolib/config.yml +19 -0
- biolib/_internal/templates/init_template/Dockerfile +14 -0
- biolib/_internal/templates/init_template/requirements.txt +1 -0
- biolib/_internal/templates/init_template/run.py +12 -0
- biolib/_internal/templates/init_template/run.sh +4 -0
- biolib/_internal/templates/templates.py +25 -0
- biolib/_internal/tree_utils.py +106 -0
- biolib/_internal/utils/__init__.py +65 -0
- biolib/_internal/utils/auth.py +46 -0
- biolib/_internal/utils/job_url.py +33 -0
- biolib/_internal/utils/multinode.py +263 -0
- biolib/_runtime/runtime.py +157 -0
- biolib/_session/session.py +44 -0
- biolib/_shared/__init__.py +0 -0
- biolib/_shared/types/__init__.py +74 -0
- biolib/_shared/types/account.py +12 -0
- biolib/_shared/types/account_member.py +8 -0
- biolib/_shared/types/app.py +9 -0
- biolib/_shared/types/data_record.py +40 -0
- biolib/_shared/types/experiment.py +32 -0
- biolib/_shared/types/file_node.py +17 -0
- biolib/_shared/types/push.py +6 -0
- biolib/_shared/types/resource.py +37 -0
- biolib/_shared/types/resource_deploy_key.py +11 -0
- biolib/_shared/types/resource_permission.py +14 -0
- biolib/_shared/types/resource_version.py +19 -0
- biolib/_shared/types/result.py +14 -0
- biolib/_shared/types/typing.py +10 -0
- biolib/_shared/types/user.py +19 -0
- biolib/_shared/utils/__init__.py +7 -0
- biolib/_shared/utils/resource_uri.py +75 -0
- biolib/api/__init__.py +6 -0
- biolib/api/client.py +168 -0
- biolib/app/app.py +252 -49
- biolib/app/search_apps.py +45 -0
- biolib/biolib_api_client/api_client.py +126 -31
- biolib/biolib_api_client/app_types.py +24 -4
- biolib/biolib_api_client/auth.py +31 -8
- biolib/biolib_api_client/biolib_app_api.py +147 -52
- biolib/biolib_api_client/biolib_job_api.py +161 -141
- biolib/biolib_api_client/job_types.py +21 -5
- biolib/biolib_api_client/lfs_types.py +7 -23
- biolib/biolib_api_client/user_state.py +56 -0
- biolib/biolib_binary_format/__init__.py +1 -4
- biolib/biolib_binary_format/file_in_container.py +105 -0
- biolib/biolib_binary_format/module_input.py +24 -7
- biolib/biolib_binary_format/module_output_v2.py +149 -0
- biolib/biolib_binary_format/remote_endpoints.py +34 -0
- biolib/biolib_binary_format/remote_stream_seeker.py +59 -0
- biolib/biolib_binary_format/saved_job.py +3 -2
- biolib/biolib_binary_format/{attestation_document.py → stdout_and_stderr.py} +8 -8
- biolib/biolib_binary_format/system_status_update.py +3 -2
- biolib/biolib_binary_format/utils.py +175 -0
- biolib/biolib_docker_client/__init__.py +11 -2
- biolib/biolib_errors.py +36 -0
- biolib/biolib_logging.py +27 -10
- biolib/cli/__init__.py +38 -0
- biolib/cli/auth.py +46 -0
- biolib/cli/data_record.py +164 -0
- biolib/cli/index.py +32 -0
- biolib/cli/init.py +421 -0
- biolib/cli/lfs.py +101 -0
- biolib/cli/push.py +50 -0
- biolib/cli/run.py +63 -0
- biolib/cli/runtime.py +14 -0
- biolib/cli/sdk.py +16 -0
- biolib/cli/start.py +56 -0
- biolib/compute_node/cloud_utils/cloud_utils.py +110 -161
- biolib/compute_node/job_worker/cache_state.py +66 -88
- biolib/compute_node/job_worker/cache_types.py +1 -6
- biolib/compute_node/job_worker/docker_image_cache.py +112 -37
- biolib/compute_node/job_worker/executors/__init__.py +0 -3
- biolib/compute_node/job_worker/executors/docker_executor.py +532 -199
- biolib/compute_node/job_worker/executors/docker_types.py +9 -1
- biolib/compute_node/job_worker/executors/types.py +19 -9
- biolib/compute_node/job_worker/job_legacy_input_wait_timeout_thread.py +30 -0
- biolib/compute_node/job_worker/job_max_runtime_timer_thread.py +3 -5
- biolib/compute_node/job_worker/job_storage.py +108 -0
- biolib/compute_node/job_worker/job_worker.py +397 -212
- biolib/compute_node/job_worker/large_file_system.py +87 -38
- biolib/compute_node/job_worker/network_alloc.py +99 -0
- biolib/compute_node/job_worker/network_buffer.py +240 -0
- biolib/compute_node/job_worker/utilization_reporter_thread.py +197 -0
- biolib/compute_node/job_worker/utils.py +9 -24
- biolib/compute_node/remote_host_proxy.py +400 -98
- biolib/compute_node/utils.py +31 -9
- biolib/compute_node/webserver/compute_node_results_proxy.py +189 -0
- biolib/compute_node/webserver/proxy_utils.py +28 -0
- biolib/compute_node/webserver/webserver.py +130 -44
- biolib/compute_node/webserver/webserver_types.py +2 -6
- biolib/compute_node/webserver/webserver_utils.py +77 -12
- biolib/compute_node/webserver/worker_thread.py +183 -42
- biolib/experiments/__init__.py +0 -0
- biolib/experiments/experiment.py +356 -0
- biolib/jobs/__init__.py +1 -0
- biolib/jobs/job.py +741 -0
- biolib/jobs/job_result.py +185 -0
- biolib/jobs/types.py +50 -0
- biolib/py.typed +0 -0
- biolib/runtime/__init__.py +14 -0
- biolib/sdk/__init__.py +91 -0
- biolib/tables.py +34 -0
- biolib/typing_utils.py +2 -7
- biolib/user/__init__.py +1 -0
- biolib/user/sign_in.py +54 -0
- biolib/utils/__init__.py +162 -0
- biolib/utils/cache_state.py +94 -0
- biolib/utils/multipart_uploader.py +194 -0
- biolib/utils/seq_util.py +150 -0
- biolib/utils/zip/remote_zip.py +640 -0
- pybiolib-1.2.1890.dist-info/METADATA +41 -0
- pybiolib-1.2.1890.dist-info/RECORD +177 -0
- {pybiolib-0.2.951.dist-info → pybiolib-1.2.1890.dist-info}/WHEEL +1 -1
- pybiolib-1.2.1890.dist-info/entry_points.txt +2 -0
- README.md +0 -17
- biolib/app/app_result.py +0 -68
- biolib/app/utils.py +0 -62
- biolib/biolib-js/0-biolib.worker.js +0 -1
- biolib/biolib-js/1-biolib.worker.js +0 -1
- biolib/biolib-js/2-biolib.worker.js +0 -1
- biolib/biolib-js/3-biolib.worker.js +0 -1
- biolib/biolib-js/4-biolib.worker.js +0 -1
- biolib/biolib-js/5-biolib.worker.js +0 -1
- biolib/biolib-js/6-biolib.worker.js +0 -1
- biolib/biolib-js/index.html +0 -10
- biolib/biolib-js/main-biolib.js +0 -1
- biolib/biolib_api_client/biolib_account_api.py +0 -21
- biolib/biolib_api_client/biolib_large_file_system_api.py +0 -108
- biolib/biolib_binary_format/aes_encrypted_package.py +0 -42
- biolib/biolib_binary_format/module_output.py +0 -58
- biolib/biolib_binary_format/rsa_encrypted_aes_package.py +0 -57
- biolib/biolib_push.py +0 -114
- biolib/cli.py +0 -203
- biolib/cli_utils.py +0 -273
- biolib/compute_node/cloud_utils/enclave_parent_types.py +0 -7
- biolib/compute_node/enclave/__init__.py +0 -2
- biolib/compute_node/enclave/enclave_remote_hosts.py +0 -53
- biolib/compute_node/enclave/nitro_secure_module_utils.py +0 -64
- biolib/compute_node/job_worker/executors/base_executor.py +0 -18
- biolib/compute_node/job_worker/executors/pyppeteer_executor.py +0 -173
- biolib/compute_node/job_worker/executors/remote/__init__.py +0 -1
- biolib/compute_node/job_worker/executors/remote/nitro_enclave_utils.py +0 -81
- biolib/compute_node/job_worker/executors/remote/remote_executor.py +0 -51
- biolib/lfs.py +0 -196
- biolib/pyppeteer/.circleci/config.yml +0 -100
- biolib/pyppeteer/.coveragerc +0 -3
- biolib/pyppeteer/.gitignore +0 -89
- biolib/pyppeteer/.pre-commit-config.yaml +0 -28
- biolib/pyppeteer/CHANGES.md +0 -253
- biolib/pyppeteer/CONTRIBUTING.md +0 -26
- biolib/pyppeteer/LICENSE +0 -12
- biolib/pyppeteer/README.md +0 -137
- biolib/pyppeteer/docs/Makefile +0 -177
- biolib/pyppeteer/docs/_static/custom.css +0 -28
- biolib/pyppeteer/docs/_templates/layout.html +0 -10
- biolib/pyppeteer/docs/changes.md +0 -1
- biolib/pyppeteer/docs/conf.py +0 -299
- biolib/pyppeteer/docs/index.md +0 -21
- biolib/pyppeteer/docs/make.bat +0 -242
- biolib/pyppeteer/docs/reference.md +0 -211
- biolib/pyppeteer/docs/server.py +0 -60
- biolib/pyppeteer/poetry.lock +0 -1699
- biolib/pyppeteer/pyppeteer/__init__.py +0 -135
- biolib/pyppeteer/pyppeteer/accessibility.py +0 -286
- biolib/pyppeteer/pyppeteer/browser.py +0 -401
- biolib/pyppeteer/pyppeteer/browser_fetcher.py +0 -194
- biolib/pyppeteer/pyppeteer/command.py +0 -22
- biolib/pyppeteer/pyppeteer/connection/__init__.py +0 -242
- biolib/pyppeteer/pyppeteer/connection/cdpsession.py +0 -101
- biolib/pyppeteer/pyppeteer/coverage.py +0 -346
- biolib/pyppeteer/pyppeteer/device_descriptors.py +0 -787
- biolib/pyppeteer/pyppeteer/dialog.py +0 -79
- biolib/pyppeteer/pyppeteer/domworld.py +0 -597
- biolib/pyppeteer/pyppeteer/emulation_manager.py +0 -53
- biolib/pyppeteer/pyppeteer/errors.py +0 -48
- biolib/pyppeteer/pyppeteer/events.py +0 -63
- biolib/pyppeteer/pyppeteer/execution_context.py +0 -156
- biolib/pyppeteer/pyppeteer/frame/__init__.py +0 -299
- biolib/pyppeteer/pyppeteer/frame/frame_manager.py +0 -306
- biolib/pyppeteer/pyppeteer/helpers.py +0 -245
- biolib/pyppeteer/pyppeteer/input.py +0 -371
- biolib/pyppeteer/pyppeteer/jshandle.py +0 -598
- biolib/pyppeteer/pyppeteer/launcher.py +0 -683
- biolib/pyppeteer/pyppeteer/lifecycle_watcher.py +0 -169
- biolib/pyppeteer/pyppeteer/models/__init__.py +0 -103
- biolib/pyppeteer/pyppeteer/models/_protocol.py +0 -12460
- biolib/pyppeteer/pyppeteer/multimap.py +0 -82
- biolib/pyppeteer/pyppeteer/network_manager.py +0 -678
- biolib/pyppeteer/pyppeteer/options.py +0 -8
- biolib/pyppeteer/pyppeteer/page.py +0 -1728
- biolib/pyppeteer/pyppeteer/pipe_transport.py +0 -59
- biolib/pyppeteer/pyppeteer/target.py +0 -147
- biolib/pyppeteer/pyppeteer/task_queue.py +0 -24
- biolib/pyppeteer/pyppeteer/timeout_settings.py +0 -36
- biolib/pyppeteer/pyppeteer/tracing.py +0 -93
- biolib/pyppeteer/pyppeteer/us_keyboard_layout.py +0 -305
- biolib/pyppeteer/pyppeteer/util.py +0 -18
- biolib/pyppeteer/pyppeteer/websocket_transport.py +0 -47
- biolib/pyppeteer/pyppeteer/worker.py +0 -101
- biolib/pyppeteer/pyproject.toml +0 -97
- biolib/pyppeteer/spell.txt +0 -137
- biolib/pyppeteer/tox.ini +0 -72
- biolib/pyppeteer/utils/generate_protocol_types.py +0 -603
- biolib/start_cli.py +0 -7
- biolib/utils.py +0 -47
- biolib/validators/validate_app_version.py +0 -183
- biolib/validators/validate_argument.py +0 -134
- biolib/validators/validate_module.py +0 -323
- biolib/validators/validate_zip_file.py +0 -40
- biolib/validators/validator_utils.py +0 -103
- pybiolib-0.2.951.dist-info/LICENSE +0 -21
- pybiolib-0.2.951.dist-info/METADATA +0 -61
- pybiolib-0.2.951.dist-info/RECORD +0 -153
- pybiolib-0.2.951.dist-info/entry_points.txt +0 -3
- /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
|
|
9
|
-
|
|
10
|
-
from docker.
|
|
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.
|
|
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.
|
|
20
|
-
from biolib.compute_node.job_worker.executors.types import
|
|
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
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
60
|
+
self._compute_process_dir = os.path.dirname(os.path.realpath(__file__))
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
56
|
-
self.
|
|
57
|
-
self.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
self.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
def _pull(self):
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
287
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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.
|
|
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
|
-
|
|
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')
|