flyte 0.0.1b0__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.
Potentially problematic release.
This version of flyte might be problematic. Click here for more details.
- flyte/__init__.py +62 -0
- flyte/_api_commons.py +3 -0
- flyte/_bin/__init__.py +0 -0
- flyte/_bin/runtime.py +126 -0
- flyte/_build.py +25 -0
- flyte/_cache/__init__.py +12 -0
- flyte/_cache/cache.py +146 -0
- flyte/_cache/defaults.py +9 -0
- flyte/_cache/policy_function_body.py +42 -0
- flyte/_cli/__init__.py +0 -0
- flyte/_cli/_common.py +287 -0
- flyte/_cli/_create.py +42 -0
- flyte/_cli/_delete.py +23 -0
- flyte/_cli/_deploy.py +140 -0
- flyte/_cli/_get.py +235 -0
- flyte/_cli/_run.py +152 -0
- flyte/_cli/main.py +72 -0
- flyte/_code_bundle/__init__.py +8 -0
- flyte/_code_bundle/_ignore.py +113 -0
- flyte/_code_bundle/_packaging.py +187 -0
- flyte/_code_bundle/_utils.py +339 -0
- flyte/_code_bundle/bundle.py +178 -0
- flyte/_context.py +146 -0
- flyte/_datastructures.py +342 -0
- flyte/_deploy.py +202 -0
- flyte/_doc.py +29 -0
- flyte/_docstring.py +32 -0
- flyte/_environment.py +43 -0
- flyte/_group.py +31 -0
- flyte/_hash.py +23 -0
- flyte/_image.py +760 -0
- flyte/_initialize.py +634 -0
- flyte/_interface.py +84 -0
- flyte/_internal/__init__.py +3 -0
- flyte/_internal/controllers/__init__.py +115 -0
- flyte/_internal/controllers/_local_controller.py +118 -0
- flyte/_internal/controllers/_trace.py +40 -0
- flyte/_internal/controllers/pbhash.py +39 -0
- flyte/_internal/controllers/remote/__init__.py +40 -0
- flyte/_internal/controllers/remote/_action.py +141 -0
- flyte/_internal/controllers/remote/_client.py +43 -0
- flyte/_internal/controllers/remote/_controller.py +361 -0
- flyte/_internal/controllers/remote/_core.py +402 -0
- flyte/_internal/controllers/remote/_informer.py +361 -0
- flyte/_internal/controllers/remote/_service_protocol.py +50 -0
- flyte/_internal/imagebuild/__init__.py +11 -0
- flyte/_internal/imagebuild/docker_builder.py +416 -0
- flyte/_internal/imagebuild/image_builder.py +241 -0
- flyte/_internal/imagebuild/remote_builder.py +0 -0
- flyte/_internal/resolvers/__init__.py +0 -0
- flyte/_internal/resolvers/_task_module.py +54 -0
- flyte/_internal/resolvers/common.py +31 -0
- flyte/_internal/resolvers/default.py +28 -0
- flyte/_internal/runtime/__init__.py +0 -0
- flyte/_internal/runtime/convert.py +199 -0
- flyte/_internal/runtime/entrypoints.py +135 -0
- flyte/_internal/runtime/io.py +136 -0
- flyte/_internal/runtime/resources_serde.py +138 -0
- flyte/_internal/runtime/task_serde.py +210 -0
- flyte/_internal/runtime/taskrunner.py +190 -0
- flyte/_internal/runtime/types_serde.py +54 -0
- flyte/_logging.py +124 -0
- flyte/_protos/__init__.py +0 -0
- flyte/_protos/common/authorization_pb2.py +66 -0
- flyte/_protos/common/authorization_pb2.pyi +108 -0
- flyte/_protos/common/authorization_pb2_grpc.py +4 -0
- flyte/_protos/common/identifier_pb2.py +71 -0
- flyte/_protos/common/identifier_pb2.pyi +82 -0
- flyte/_protos/common/identifier_pb2_grpc.py +4 -0
- flyte/_protos/common/identity_pb2.py +48 -0
- flyte/_protos/common/identity_pb2.pyi +72 -0
- flyte/_protos/common/identity_pb2_grpc.py +4 -0
- flyte/_protos/common/list_pb2.py +36 -0
- flyte/_protos/common/list_pb2.pyi +69 -0
- flyte/_protos/common/list_pb2_grpc.py +4 -0
- flyte/_protos/common/policy_pb2.py +37 -0
- flyte/_protos/common/policy_pb2.pyi +27 -0
- flyte/_protos/common/policy_pb2_grpc.py +4 -0
- flyte/_protos/common/role_pb2.py +37 -0
- flyte/_protos/common/role_pb2.pyi +53 -0
- flyte/_protos/common/role_pb2_grpc.py +4 -0
- flyte/_protos/common/runtime_version_pb2.py +28 -0
- flyte/_protos/common/runtime_version_pb2.pyi +24 -0
- flyte/_protos/common/runtime_version_pb2_grpc.py +4 -0
- flyte/_protos/logs/dataplane/payload_pb2.py +96 -0
- flyte/_protos/logs/dataplane/payload_pb2.pyi +168 -0
- flyte/_protos/logs/dataplane/payload_pb2_grpc.py +4 -0
- flyte/_protos/secret/definition_pb2.py +49 -0
- flyte/_protos/secret/definition_pb2.pyi +93 -0
- flyte/_protos/secret/definition_pb2_grpc.py +4 -0
- flyte/_protos/secret/payload_pb2.py +62 -0
- flyte/_protos/secret/payload_pb2.pyi +94 -0
- flyte/_protos/secret/payload_pb2_grpc.py +4 -0
- flyte/_protos/secret/secret_pb2.py +38 -0
- flyte/_protos/secret/secret_pb2.pyi +6 -0
- flyte/_protos/secret/secret_pb2_grpc.py +198 -0
- flyte/_protos/secret/secret_pb2_grpc_grpc.py +198 -0
- flyte/_protos/validate/validate/validate_pb2.py +76 -0
- flyte/_protos/workflow/node_execution_service_pb2.py +26 -0
- flyte/_protos/workflow/node_execution_service_pb2.pyi +4 -0
- flyte/_protos/workflow/node_execution_service_pb2_grpc.py +32 -0
- flyte/_protos/workflow/queue_service_pb2.py +106 -0
- flyte/_protos/workflow/queue_service_pb2.pyi +141 -0
- flyte/_protos/workflow/queue_service_pb2_grpc.py +172 -0
- flyte/_protos/workflow/run_definition_pb2.py +128 -0
- flyte/_protos/workflow/run_definition_pb2.pyi +310 -0
- flyte/_protos/workflow/run_definition_pb2_grpc.py +4 -0
- flyte/_protos/workflow/run_logs_service_pb2.py +41 -0
- flyte/_protos/workflow/run_logs_service_pb2.pyi +28 -0
- flyte/_protos/workflow/run_logs_service_pb2_grpc.py +69 -0
- flyte/_protos/workflow/run_service_pb2.py +133 -0
- flyte/_protos/workflow/run_service_pb2.pyi +175 -0
- flyte/_protos/workflow/run_service_pb2_grpc.py +412 -0
- flyte/_protos/workflow/state_service_pb2.py +58 -0
- flyte/_protos/workflow/state_service_pb2.pyi +71 -0
- flyte/_protos/workflow/state_service_pb2_grpc.py +138 -0
- flyte/_protos/workflow/task_definition_pb2.py +72 -0
- flyte/_protos/workflow/task_definition_pb2.pyi +65 -0
- flyte/_protos/workflow/task_definition_pb2_grpc.py +4 -0
- flyte/_protos/workflow/task_service_pb2.py +44 -0
- flyte/_protos/workflow/task_service_pb2.pyi +31 -0
- flyte/_protos/workflow/task_service_pb2_grpc.py +104 -0
- flyte/_resources.py +226 -0
- flyte/_retry.py +32 -0
- flyte/_reusable_environment.py +25 -0
- flyte/_run.py +411 -0
- flyte/_secret.py +61 -0
- flyte/_task.py +367 -0
- flyte/_task_environment.py +200 -0
- flyte/_timeout.py +47 -0
- flyte/_tools.py +27 -0
- flyte/_trace.py +128 -0
- flyte/_utils/__init__.py +20 -0
- flyte/_utils/asyn.py +119 -0
- flyte/_utils/coro_management.py +25 -0
- flyte/_utils/file_handling.py +72 -0
- flyte/_utils/helpers.py +108 -0
- flyte/_utils/lazy_module.py +54 -0
- flyte/_utils/uv_script_parser.py +49 -0
- flyte/_version.py +21 -0
- flyte/connectors/__init__.py +0 -0
- flyte/errors.py +143 -0
- flyte/extras/__init__.py +5 -0
- flyte/extras/_container.py +273 -0
- flyte/io/__init__.py +11 -0
- flyte/io/_dataframe.py +0 -0
- flyte/io/_dir.py +448 -0
- flyte/io/_file.py +468 -0
- flyte/io/pickle/__init__.py +0 -0
- flyte/io/pickle/transformer.py +117 -0
- flyte/io/structured_dataset/__init__.py +129 -0
- flyte/io/structured_dataset/basic_dfs.py +219 -0
- flyte/io/structured_dataset/structured_dataset.py +1061 -0
- flyte/py.typed +0 -0
- flyte/remote/__init__.py +25 -0
- flyte/remote/_client/__init__.py +0 -0
- flyte/remote/_client/_protocols.py +131 -0
- flyte/remote/_client/auth/__init__.py +12 -0
- flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
- flyte/remote/_client/auth/_authenticators/base.py +397 -0
- flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
- flyte/remote/_client/auth/_authenticators/device_code.py +118 -0
- flyte/remote/_client/auth/_authenticators/external_command.py +79 -0
- flyte/remote/_client/auth/_authenticators/factory.py +200 -0
- flyte/remote/_client/auth/_authenticators/pkce.py +516 -0
- flyte/remote/_client/auth/_channel.py +184 -0
- flyte/remote/_client/auth/_client_config.py +83 -0
- flyte/remote/_client/auth/_default_html.py +32 -0
- flyte/remote/_client/auth/_grpc_utils/__init__.py +0 -0
- flyte/remote/_client/auth/_grpc_utils/auth_interceptor.py +288 -0
- flyte/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +151 -0
- flyte/remote/_client/auth/_keyring.py +143 -0
- flyte/remote/_client/auth/_token_client.py +260 -0
- flyte/remote/_client/auth/errors.py +16 -0
- flyte/remote/_client/controlplane.py +95 -0
- flyte/remote/_console.py +18 -0
- flyte/remote/_data.py +155 -0
- flyte/remote/_logs.py +116 -0
- flyte/remote/_project.py +86 -0
- flyte/remote/_run.py +873 -0
- flyte/remote/_secret.py +132 -0
- flyte/remote/_task.py +227 -0
- flyte/report/__init__.py +3 -0
- flyte/report/_report.py +178 -0
- flyte/report/_template.html +124 -0
- flyte/storage/__init__.py +24 -0
- flyte/storage/_remote_fs.py +34 -0
- flyte/storage/_storage.py +251 -0
- flyte/storage/_utils.py +5 -0
- flyte/types/__init__.py +13 -0
- flyte/types/_interface.py +25 -0
- flyte/types/_renderer.py +162 -0
- flyte/types/_string_literals.py +120 -0
- flyte/types/_type_engine.py +2210 -0
- flyte/types/_utils.py +80 -0
- flyte-0.0.1b0.dist-info/METADATA +179 -0
- flyte-0.0.1b0.dist-info/RECORD +390 -0
- flyte-0.0.1b0.dist-info/WHEEL +5 -0
- flyte-0.0.1b0.dist-info/entry_points.txt +3 -0
- flyte-0.0.1b0.dist-info/top_level.txt +1 -0
- union/__init__.py +54 -0
- union/_api_commons.py +3 -0
- union/_bin/__init__.py +0 -0
- union/_bin/runtime.py +113 -0
- union/_build.py +25 -0
- union/_cache/__init__.py +12 -0
- union/_cache/cache.py +141 -0
- union/_cache/defaults.py +9 -0
- union/_cache/policy_function_body.py +42 -0
- union/_cli/__init__.py +0 -0
- union/_cli/_common.py +263 -0
- union/_cli/_create.py +40 -0
- union/_cli/_delete.py +23 -0
- union/_cli/_deploy.py +120 -0
- union/_cli/_get.py +162 -0
- union/_cli/_params.py +579 -0
- union/_cli/_run.py +150 -0
- union/_cli/main.py +72 -0
- union/_code_bundle/__init__.py +8 -0
- union/_code_bundle/_ignore.py +113 -0
- union/_code_bundle/_packaging.py +187 -0
- union/_code_bundle/_utils.py +342 -0
- union/_code_bundle/bundle.py +176 -0
- union/_context.py +146 -0
- union/_datastructures.py +295 -0
- union/_deploy.py +185 -0
- union/_doc.py +29 -0
- union/_docstring.py +26 -0
- union/_environment.py +43 -0
- union/_group.py +31 -0
- union/_hash.py +23 -0
- union/_image.py +760 -0
- union/_initialize.py +585 -0
- union/_interface.py +84 -0
- union/_internal/__init__.py +3 -0
- union/_internal/controllers/__init__.py +77 -0
- union/_internal/controllers/_local_controller.py +77 -0
- union/_internal/controllers/pbhash.py +39 -0
- union/_internal/controllers/remote/__init__.py +40 -0
- union/_internal/controllers/remote/_action.py +131 -0
- union/_internal/controllers/remote/_client.py +43 -0
- union/_internal/controllers/remote/_controller.py +169 -0
- union/_internal/controllers/remote/_core.py +341 -0
- union/_internal/controllers/remote/_informer.py +260 -0
- union/_internal/controllers/remote/_service_protocol.py +44 -0
- union/_internal/imagebuild/__init__.py +11 -0
- union/_internal/imagebuild/docker_builder.py +416 -0
- union/_internal/imagebuild/image_builder.py +243 -0
- union/_internal/imagebuild/remote_builder.py +0 -0
- union/_internal/resolvers/__init__.py +0 -0
- union/_internal/resolvers/_task_module.py +31 -0
- union/_internal/resolvers/common.py +24 -0
- union/_internal/resolvers/default.py +27 -0
- union/_internal/runtime/__init__.py +0 -0
- union/_internal/runtime/convert.py +163 -0
- union/_internal/runtime/entrypoints.py +121 -0
- union/_internal/runtime/io.py +136 -0
- union/_internal/runtime/resources_serde.py +134 -0
- union/_internal/runtime/task_serde.py +202 -0
- union/_internal/runtime/taskrunner.py +179 -0
- union/_internal/runtime/types_serde.py +53 -0
- union/_logging.py +124 -0
- union/_protos/__init__.py +0 -0
- union/_protos/common/authorization_pb2.py +66 -0
- union/_protos/common/authorization_pb2.pyi +106 -0
- union/_protos/common/authorization_pb2_grpc.py +4 -0
- union/_protos/common/identifier_pb2.py +71 -0
- union/_protos/common/identifier_pb2.pyi +82 -0
- union/_protos/common/identifier_pb2_grpc.py +4 -0
- union/_protos/common/identity_pb2.py +48 -0
- union/_protos/common/identity_pb2.pyi +72 -0
- union/_protos/common/identity_pb2_grpc.py +4 -0
- union/_protos/common/list_pb2.py +36 -0
- union/_protos/common/list_pb2.pyi +69 -0
- union/_protos/common/list_pb2_grpc.py +4 -0
- union/_protos/common/policy_pb2.py +37 -0
- union/_protos/common/policy_pb2.pyi +27 -0
- union/_protos/common/policy_pb2_grpc.py +4 -0
- union/_protos/common/role_pb2.py +37 -0
- union/_protos/common/role_pb2.pyi +51 -0
- union/_protos/common/role_pb2_grpc.py +4 -0
- union/_protos/common/runtime_version_pb2.py +28 -0
- union/_protos/common/runtime_version_pb2.pyi +24 -0
- union/_protos/common/runtime_version_pb2_grpc.py +4 -0
- union/_protos/logs/dataplane/payload_pb2.py +96 -0
- union/_protos/logs/dataplane/payload_pb2.pyi +168 -0
- union/_protos/logs/dataplane/payload_pb2_grpc.py +4 -0
- union/_protos/secret/definition_pb2.py +49 -0
- union/_protos/secret/definition_pb2.pyi +93 -0
- union/_protos/secret/definition_pb2_grpc.py +4 -0
- union/_protos/secret/payload_pb2.py +62 -0
- union/_protos/secret/payload_pb2.pyi +94 -0
- union/_protos/secret/payload_pb2_grpc.py +4 -0
- union/_protos/secret/secret_pb2.py +38 -0
- union/_protos/secret/secret_pb2.pyi +6 -0
- union/_protos/secret/secret_pb2_grpc.py +198 -0
- union/_protos/validate/validate/validate_pb2.py +76 -0
- union/_protos/workflow/node_execution_service_pb2.py +26 -0
- union/_protos/workflow/node_execution_service_pb2.pyi +4 -0
- union/_protos/workflow/node_execution_service_pb2_grpc.py +32 -0
- union/_protos/workflow/queue_service_pb2.py +75 -0
- union/_protos/workflow/queue_service_pb2.pyi +103 -0
- union/_protos/workflow/queue_service_pb2_grpc.py +172 -0
- union/_protos/workflow/run_definition_pb2.py +100 -0
- union/_protos/workflow/run_definition_pb2.pyi +256 -0
- union/_protos/workflow/run_definition_pb2_grpc.py +4 -0
- union/_protos/workflow/run_logs_service_pb2.py +41 -0
- union/_protos/workflow/run_logs_service_pb2.pyi +28 -0
- union/_protos/workflow/run_logs_service_pb2_grpc.py +69 -0
- union/_protos/workflow/run_service_pb2.py +133 -0
- union/_protos/workflow/run_service_pb2.pyi +173 -0
- union/_protos/workflow/run_service_pb2_grpc.py +412 -0
- union/_protos/workflow/state_service_pb2.py +58 -0
- union/_protos/workflow/state_service_pb2.pyi +69 -0
- union/_protos/workflow/state_service_pb2_grpc.py +138 -0
- union/_protos/workflow/task_definition_pb2.py +72 -0
- union/_protos/workflow/task_definition_pb2.pyi +65 -0
- union/_protos/workflow/task_definition_pb2_grpc.py +4 -0
- union/_protos/workflow/task_service_pb2.py +44 -0
- union/_protos/workflow/task_service_pb2.pyi +31 -0
- union/_protos/workflow/task_service_pb2_grpc.py +104 -0
- union/_resources.py +226 -0
- union/_retry.py +32 -0
- union/_reusable_environment.py +25 -0
- union/_run.py +374 -0
- union/_secret.py +61 -0
- union/_task.py +354 -0
- union/_task_environment.py +186 -0
- union/_timeout.py +47 -0
- union/_tools.py +27 -0
- union/_utils/__init__.py +11 -0
- union/_utils/asyn.py +119 -0
- union/_utils/file_handling.py +71 -0
- union/_utils/helpers.py +46 -0
- union/_utils/lazy_module.py +54 -0
- union/_utils/uv_script_parser.py +49 -0
- union/_version.py +21 -0
- union/connectors/__init__.py +0 -0
- union/errors.py +128 -0
- union/extras/__init__.py +5 -0
- union/extras/_container.py +263 -0
- union/io/__init__.py +11 -0
- union/io/_dataframe.py +0 -0
- union/io/_dir.py +425 -0
- union/io/_file.py +418 -0
- union/io/pickle/__init__.py +0 -0
- union/io/pickle/transformer.py +117 -0
- union/io/structured_dataset/__init__.py +122 -0
- union/io/structured_dataset/basic_dfs.py +219 -0
- union/io/structured_dataset/structured_dataset.py +1057 -0
- union/py.typed +0 -0
- union/remote/__init__.py +23 -0
- union/remote/_client/__init__.py +0 -0
- union/remote/_client/_protocols.py +129 -0
- union/remote/_client/auth/__init__.py +12 -0
- union/remote/_client/auth/_authenticators/__init__.py +0 -0
- union/remote/_client/auth/_authenticators/base.py +391 -0
- union/remote/_client/auth/_authenticators/client_credentials.py +73 -0
- union/remote/_client/auth/_authenticators/device_code.py +120 -0
- union/remote/_client/auth/_authenticators/external_command.py +77 -0
- union/remote/_client/auth/_authenticators/factory.py +200 -0
- union/remote/_client/auth/_authenticators/pkce.py +515 -0
- union/remote/_client/auth/_channel.py +184 -0
- union/remote/_client/auth/_client_config.py +83 -0
- union/remote/_client/auth/_default_html.py +32 -0
- union/remote/_client/auth/_grpc_utils/__init__.py +0 -0
- union/remote/_client/auth/_grpc_utils/auth_interceptor.py +204 -0
- union/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +144 -0
- union/remote/_client/auth/_keyring.py +154 -0
- union/remote/_client/auth/_token_client.py +258 -0
- union/remote/_client/auth/errors.py +16 -0
- union/remote/_client/controlplane.py +86 -0
- union/remote/_data.py +149 -0
- union/remote/_logs.py +74 -0
- union/remote/_project.py +86 -0
- union/remote/_run.py +820 -0
- union/remote/_secret.py +132 -0
- union/remote/_task.py +193 -0
- union/report/__init__.py +3 -0
- union/report/_report.py +178 -0
- union/report/_template.html +124 -0
- union/storage/__init__.py +24 -0
- union/storage/_remote_fs.py +34 -0
- union/storage/_storage.py +247 -0
- union/storage/_utils.py +5 -0
- union/types/__init__.py +11 -0
- union/types/_renderer.py +162 -0
- union/types/_string_literals.py +120 -0
- union/types/_type_engine.py +2131 -0
- union/types/_utils.py +80 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from string import Template
|
|
7
|
+
from typing import ClassVar, Protocol
|
|
8
|
+
|
|
9
|
+
import aiofiles
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from union._image import (
|
|
13
|
+
AptPackages,
|
|
14
|
+
Commands,
|
|
15
|
+
CopyConfig,
|
|
16
|
+
Env,
|
|
17
|
+
Image,
|
|
18
|
+
Layer,
|
|
19
|
+
PipPackages,
|
|
20
|
+
Requirements,
|
|
21
|
+
UVProject,
|
|
22
|
+
WorkDir,
|
|
23
|
+
_DockerLines,
|
|
24
|
+
)
|
|
25
|
+
from union._logging import logger
|
|
26
|
+
|
|
27
|
+
_F_IMG_ID = "_F_IMG_ID"
|
|
28
|
+
|
|
29
|
+
UV_LOCK_INSTALL_TEMPLATE = Template("""\
|
|
30
|
+
WORKDIR /root
|
|
31
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
|
|
32
|
+
--mount=type=bind,target=uv.lock,src=uv.lock \
|
|
33
|
+
--mount=type=bind,target=pyproject.toml,src=pyproject.toml \
|
|
34
|
+
uv sync $PIP_INSTALL_ARGS
|
|
35
|
+
WORKDIR /
|
|
36
|
+
|
|
37
|
+
# Update PATH and UV_PYTHON to point to the venv created by uv sync
|
|
38
|
+
ENV PATH="/root/.venv/bin:$$PATH" \
|
|
39
|
+
VIRTUALENV=/root/.venv \
|
|
40
|
+
UV_PYTHON=/root/.venv/bin/python
|
|
41
|
+
""")
|
|
42
|
+
|
|
43
|
+
UV_PACKAGE_INSTALL_COMMAND_TEMPLATE = Template("""\
|
|
44
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
|
|
45
|
+
--mount=type=bind,target=requirements_uv.txt,src=requirements_uv.txt \
|
|
46
|
+
uv pip install --python $$UV_PYTHON $PIP_INSTALL_ARGS
|
|
47
|
+
""")
|
|
48
|
+
|
|
49
|
+
APT_INSTALL_COMMAND_TEMPLATE = Template("""\
|
|
50
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/var/cache/apt,id=apt \
|
|
51
|
+
apt-get update && apt-get install -y --no-install-recommends \
|
|
52
|
+
$APT_PACKAGES
|
|
53
|
+
""")
|
|
54
|
+
|
|
55
|
+
UV_PYTHON_INSTALL_COMMAND = Template("""\
|
|
56
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
|
|
57
|
+
uv pip install $PIP_INSTALL_ARGS
|
|
58
|
+
""")
|
|
59
|
+
|
|
60
|
+
# uv pip install --python /root/env/bin/python
|
|
61
|
+
# new template
|
|
62
|
+
DOCKER_FILE_UV_BASE_TEMPLATE = Template("""\
|
|
63
|
+
#syntax=docker/dockerfile:1.5
|
|
64
|
+
FROM ghcr.io/astral-sh/uv:0.6.12 as uv
|
|
65
|
+
FROM $BASE_IMAGE
|
|
66
|
+
|
|
67
|
+
USER root
|
|
68
|
+
|
|
69
|
+
# Copy in uv so that later commands don't have to mount it in
|
|
70
|
+
COPY --from=uv /uv /usr/bin/uv
|
|
71
|
+
|
|
72
|
+
# Configure default envs
|
|
73
|
+
ENV UV_COMPILE_BYTECODE=1 \
|
|
74
|
+
UV_LINK_MODE=copy \
|
|
75
|
+
VIRTUALENV=/opt/venv \
|
|
76
|
+
UV_PYTHON=/opt/venv/bin/python \
|
|
77
|
+
PATH="/opt/venv/bin:$$PATH"
|
|
78
|
+
|
|
79
|
+
# Create a virtualenv with the user specified python version
|
|
80
|
+
RUN uv venv $$VIRTUALENV --python=$PYTHON_VERSION
|
|
81
|
+
|
|
82
|
+
# Adds nvidia just in case it exists
|
|
83
|
+
ENV PATH="$$PATH:/usr/local/nvidia/bin:/usr/local/cuda/bin" \
|
|
84
|
+
LD_LIBRARY_PATH="/usr/local/nvidia/lib64:$$LD_LIBRARY_PATH"
|
|
85
|
+
""")
|
|
86
|
+
|
|
87
|
+
# This gets added on to the end of the dockerfile
|
|
88
|
+
DOCKER_FILE_BASE_FOOTER = Template("""\
|
|
89
|
+
ENV _F_IMG_ID=$F_IMG_ID
|
|
90
|
+
WORKDIR /root
|
|
91
|
+
SHELL ["/bin/bash", "-c"]
|
|
92
|
+
""")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Handler(Protocol):
|
|
96
|
+
@staticmethod
|
|
97
|
+
async def handle(layer: Layer, context_path: Path, dockerfile: str) -> str: ...
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class PipAndRequirementsHandler:
|
|
101
|
+
@staticmethod
|
|
102
|
+
async def handle(layer: PipPackages, context_path: Path, dockerfile: str) -> str:
|
|
103
|
+
if isinstance(layer, Requirements):
|
|
104
|
+
async with aiofiles.open(layer.file) as f:
|
|
105
|
+
requirements = []
|
|
106
|
+
async for line in f:
|
|
107
|
+
requirement = await line
|
|
108
|
+
requirements.append(requirement.strip())
|
|
109
|
+
else:
|
|
110
|
+
requirements = layer.packages
|
|
111
|
+
requirements_uv_path = context_path / "requirements_uv.txt"
|
|
112
|
+
async with aiofiles.open(requirements_uv_path, "w") as f:
|
|
113
|
+
reqs = "\n".join(requirements)
|
|
114
|
+
await f.write(reqs)
|
|
115
|
+
|
|
116
|
+
pip_install_args = []
|
|
117
|
+
if layer.index_url:
|
|
118
|
+
pip_install_args.append(f"--index-url {layer.index_url}")
|
|
119
|
+
|
|
120
|
+
if layer.extra_index_urls:
|
|
121
|
+
pip_install_args.extend([f"--extra-index-url {url}" for url in layer.extra_index_urls])
|
|
122
|
+
|
|
123
|
+
if layer.pre:
|
|
124
|
+
pip_install_args.append("--pre")
|
|
125
|
+
|
|
126
|
+
if layer.extra_args:
|
|
127
|
+
pip_install_args.append(layer.extra_args)
|
|
128
|
+
|
|
129
|
+
pip_install_args.extend(["--requirement", "requirements_uv.txt"])
|
|
130
|
+
|
|
131
|
+
delta = UV_PACKAGE_INSTALL_COMMAND_TEMPLATE.substitute(PIP_INSTALL_ARGS=" ".join(pip_install_args))
|
|
132
|
+
dockerfile += delta
|
|
133
|
+
|
|
134
|
+
return dockerfile
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class _DockerLinesHandler:
|
|
138
|
+
@staticmethod
|
|
139
|
+
async def handle(layer: _DockerLines, context_path: Path, dockerfile: str) -> str:
|
|
140
|
+
# Add the lines to the dockerfile
|
|
141
|
+
for line in layer.lines:
|
|
142
|
+
dockerfile += f"\n{line}\n"
|
|
143
|
+
|
|
144
|
+
return dockerfile
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class EnvHandler:
|
|
148
|
+
@staticmethod
|
|
149
|
+
async def handle(layer: Env, context_path: Path, dockerfile: str) -> str:
|
|
150
|
+
# Add the env vars to the dockerfile
|
|
151
|
+
for key, value in layer.env_vars:
|
|
152
|
+
dockerfile += f"\nENV {key}={value}\n"
|
|
153
|
+
|
|
154
|
+
return dockerfile
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class AptPackagesHandler:
|
|
158
|
+
@staticmethod
|
|
159
|
+
async def handle(layer: AptPackages, context_path: Path, dockerfile: str) -> str:
|
|
160
|
+
layer = layer.packages
|
|
161
|
+
delta = APT_INSTALL_COMMAND_TEMPLATE.substitute(APT_PACKAGES=" ".join(layer))
|
|
162
|
+
dockerfile += delta
|
|
163
|
+
|
|
164
|
+
return dockerfile
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class UVProjectHandler:
|
|
168
|
+
@staticmethod
|
|
169
|
+
async def handle(layer: UVProject, context_path: Path, dockerfile: str) -> str:
|
|
170
|
+
# copy the two files
|
|
171
|
+
shutil.copy(layer.pyproject, context_path)
|
|
172
|
+
shutil.copy(layer.uvlock, context_path)
|
|
173
|
+
|
|
174
|
+
# --locked: Assert that the `uv.lock` will remain unchanged
|
|
175
|
+
# --no-dev: Omit the development dependency group
|
|
176
|
+
# --no-install-project: Do not install the current project
|
|
177
|
+
additional_pip_install_args = ["--locked", "--no-dev", "--no-install-project"]
|
|
178
|
+
delta = UV_LOCK_INSTALL_TEMPLATE.substitute(PIP_INSTALL_ARGS=" ".join(additional_pip_install_args))
|
|
179
|
+
dockerfile += delta
|
|
180
|
+
|
|
181
|
+
return dockerfile
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class CopyConfigHandler:
|
|
185
|
+
@staticmethod
|
|
186
|
+
async def handle(layer: CopyConfig, context_path: Path, dockerfile: str) -> str:
|
|
187
|
+
# Copy the source config file or directory to the context path
|
|
188
|
+
abs_path = layer.context_source.absolute()
|
|
189
|
+
dest_path = context_path / abs_path.name
|
|
190
|
+
image_dest_path = layer.image_dest + "/" + abs_path.name
|
|
191
|
+
if layer.context_source.is_file():
|
|
192
|
+
# Copy the file
|
|
193
|
+
shutil.copy(abs_path, dest_path)
|
|
194
|
+
elif layer.context_source.is_dir():
|
|
195
|
+
# Copy the entire directory
|
|
196
|
+
shutil.copytree(abs_path, dest_path)
|
|
197
|
+
else:
|
|
198
|
+
raise ValueError(f"Source path is neither file nor directory: {layer.context_source}")
|
|
199
|
+
|
|
200
|
+
# Add a copy command to the dockerfile
|
|
201
|
+
dockerfile += f"\nCOPY {abs_path.name} {image_dest_path}\n"
|
|
202
|
+
|
|
203
|
+
return dockerfile
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class CommandsHandler:
|
|
207
|
+
@staticmethod
|
|
208
|
+
async def handle(layer: Commands, context_path: Path, dockerfile: str) -> str:
|
|
209
|
+
# Append raw commands to the dockerfile
|
|
210
|
+
for command in layer.commands:
|
|
211
|
+
dockerfile += f"\nRUN {command}\n"
|
|
212
|
+
|
|
213
|
+
return dockerfile
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class WorkDirHandler:
|
|
217
|
+
@staticmethod
|
|
218
|
+
async def handle(layer: WorkDir, context_path: Path, dockerfile: str) -> str:
|
|
219
|
+
# cd to the workdir
|
|
220
|
+
dockerfile += f"\nWORKDIR {layer.workdir}\n"
|
|
221
|
+
|
|
222
|
+
return dockerfile
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def _process_layer(layer: Layer, context_path: Path, dockerfile: str) -> str:
|
|
226
|
+
match layer:
|
|
227
|
+
case Requirements() | PipPackages():
|
|
228
|
+
# Handle pip packages and requirements
|
|
229
|
+
dockerfile = await PipAndRequirementsHandler.handle(layer, context_path, dockerfile)
|
|
230
|
+
|
|
231
|
+
case AptPackages():
|
|
232
|
+
# Handle apt packages
|
|
233
|
+
dockerfile = await AptPackagesHandler.handle(layer, context_path, dockerfile)
|
|
234
|
+
|
|
235
|
+
case UVProject():
|
|
236
|
+
# Handle UV project
|
|
237
|
+
dockerfile = await UVProjectHandler.handle(layer, context_path, dockerfile)
|
|
238
|
+
|
|
239
|
+
case CopyConfig():
|
|
240
|
+
# Handle local files and folders
|
|
241
|
+
dockerfile = await CopyConfigHandler.handle(layer, context_path, dockerfile)
|
|
242
|
+
|
|
243
|
+
case Commands():
|
|
244
|
+
# Handle commands
|
|
245
|
+
dockerfile = await CommandsHandler.handle(layer, context_path, dockerfile)
|
|
246
|
+
|
|
247
|
+
case WorkDir():
|
|
248
|
+
# Handle workdir
|
|
249
|
+
dockerfile = await WorkDirHandler.handle(layer, context_path, dockerfile)
|
|
250
|
+
|
|
251
|
+
case Env():
|
|
252
|
+
# Handle environment variables
|
|
253
|
+
dockerfile = await EnvHandler.handle(layer, context_path, dockerfile)
|
|
254
|
+
|
|
255
|
+
case _DockerLines():
|
|
256
|
+
# Only for internal use
|
|
257
|
+
dockerfile = await _DockerLinesHandler.handle(layer, context_path, dockerfile)
|
|
258
|
+
|
|
259
|
+
case _:
|
|
260
|
+
raise NotImplementedError(f"Layer type {type(layer)} not supported")
|
|
261
|
+
|
|
262
|
+
return dockerfile
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class DockerImageBuilder:
|
|
266
|
+
"""Image builder using Docker and buildkit."""
|
|
267
|
+
|
|
268
|
+
builder_type: ClassVar = "docker"
|
|
269
|
+
_builder_name: ClassVar = "flytex"
|
|
270
|
+
|
|
271
|
+
async def build_image(self, image: Image, dry_run: bool = False) -> str:
|
|
272
|
+
if image.is_final:
|
|
273
|
+
if image._layers:
|
|
274
|
+
raise ValueError("Image is a default image and should already be built")
|
|
275
|
+
|
|
276
|
+
if image.dockerfile:
|
|
277
|
+
# If a dockerfile is provided, use it directly
|
|
278
|
+
return await self._build_from_dockerfile(image, push=True)
|
|
279
|
+
|
|
280
|
+
return await self._build_image(
|
|
281
|
+
image,
|
|
282
|
+
push=True,
|
|
283
|
+
dry_run=dry_run,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
async def _build_from_dockerfile(self, image: Image, push: bool) -> str:
|
|
287
|
+
"""
|
|
288
|
+
Build the image from a provided Dockerfile.
|
|
289
|
+
"""
|
|
290
|
+
command = [
|
|
291
|
+
"docker",
|
|
292
|
+
"build",
|
|
293
|
+
"--tag",
|
|
294
|
+
f"{image.uri}",
|
|
295
|
+
"--platform",
|
|
296
|
+
",".join(image.platform),
|
|
297
|
+
".",
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
if image.registry and push:
|
|
301
|
+
command.append("--push")
|
|
302
|
+
|
|
303
|
+
concat_command = " ".join(command)
|
|
304
|
+
logger.debug(f"Build command: {concat_command}")
|
|
305
|
+
click.secho(f"Run command: {concat_command} ", fg="blue")
|
|
306
|
+
|
|
307
|
+
await asyncio.to_thread(subprocess.run, command, cwd=str(image.dockerfile.cwd()), check=True)
|
|
308
|
+
|
|
309
|
+
return image.uri
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
async def _ensure_buildx_builder():
|
|
313
|
+
"""Ensure there is a docker buildx builder called flyte"""
|
|
314
|
+
# Check if buildx is available
|
|
315
|
+
try:
|
|
316
|
+
await asyncio.to_thread(
|
|
317
|
+
subprocess.run, ["docker", "buildx", "version"], check=True, stdout=subprocess.DEVNULL
|
|
318
|
+
)
|
|
319
|
+
except subprocess.CalledProcessError:
|
|
320
|
+
raise RuntimeError("Docker buildx is not available. Make sure BuildKit is installed and enabled.")
|
|
321
|
+
|
|
322
|
+
# List builders
|
|
323
|
+
result = await asyncio.to_thread(
|
|
324
|
+
subprocess.run, ["docker", "buildx", "ls"], capture_output=True, text=True, check=True
|
|
325
|
+
)
|
|
326
|
+
builders = result.stdout
|
|
327
|
+
|
|
328
|
+
# Check if there's any usable builder
|
|
329
|
+
if DockerImageBuilder._builder_name not in builders:
|
|
330
|
+
# No default builder found, create one
|
|
331
|
+
logger.info("No buildx builder found, creating one...")
|
|
332
|
+
await asyncio.to_thread(
|
|
333
|
+
subprocess.run,
|
|
334
|
+
[
|
|
335
|
+
"docker",
|
|
336
|
+
"buildx",
|
|
337
|
+
"create",
|
|
338
|
+
"--name",
|
|
339
|
+
DockerImageBuilder._builder_name,
|
|
340
|
+
"--platform",
|
|
341
|
+
"linux/amd64,linux/arm64",
|
|
342
|
+
],
|
|
343
|
+
check=True,
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
logger.info("Buildx builder already exists.")
|
|
347
|
+
|
|
348
|
+
async def _build_image(self, image: Image, *, push: bool = True, dry_run: bool = False) -> str:
|
|
349
|
+
"""
|
|
350
|
+
if default image (only base image and locked), raise an error, don't have a dockerfile
|
|
351
|
+
if dockerfile, just build
|
|
352
|
+
in the main case, get the default Dockerfile template
|
|
353
|
+
- start from the base image
|
|
354
|
+
- use python to create a default venv and export variables
|
|
355
|
+
|
|
356
|
+
Then for the layers
|
|
357
|
+
- for each layer
|
|
358
|
+
- find the appropriate layer handler
|
|
359
|
+
- call layer handler with the context dir and the dockerfile
|
|
360
|
+
- handler can choose to do something (copy files from local) to the context and update the dockerfile
|
|
361
|
+
contents, returning the new string
|
|
362
|
+
"""
|
|
363
|
+
# For testing, set `push=False` to just build the image locally and not push to
|
|
364
|
+
# registry.
|
|
365
|
+
|
|
366
|
+
await DockerImageBuilder._ensure_buildx_builder()
|
|
367
|
+
|
|
368
|
+
with tempfile.TemporaryDirectory(delete=False) as tmp_dir:
|
|
369
|
+
logger.warning(f"Temporary directory: {tmp_dir}")
|
|
370
|
+
tmp_path = Path(tmp_dir)
|
|
371
|
+
|
|
372
|
+
dockerfile = DOCKER_FILE_UV_BASE_TEMPLATE.substitute(
|
|
373
|
+
BASE_IMAGE=image.base_image,
|
|
374
|
+
PYTHON_VERSION=f"{image.python_version[0]}.{image.python_version[1]}",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
for layer in image._layers:
|
|
378
|
+
dockerfile = await _process_layer(layer, tmp_path, dockerfile)
|
|
379
|
+
|
|
380
|
+
dockerfile += DOCKER_FILE_BASE_FOOTER.substitute(F_IMG_ID=image.uri)
|
|
381
|
+
|
|
382
|
+
dockerfile_path = tmp_path / "Dockerfile"
|
|
383
|
+
async with aiofiles.open(dockerfile_path, mode="w") as f:
|
|
384
|
+
await f.write(dockerfile)
|
|
385
|
+
|
|
386
|
+
command = [
|
|
387
|
+
"docker",
|
|
388
|
+
"buildx",
|
|
389
|
+
"build",
|
|
390
|
+
"--builder",
|
|
391
|
+
DockerImageBuilder._builder_name,
|
|
392
|
+
"--tag",
|
|
393
|
+
f"{image.uri}",
|
|
394
|
+
"--platform",
|
|
395
|
+
",".join(image.platform),
|
|
396
|
+
"--push" if push else "--load",
|
|
397
|
+
]
|
|
398
|
+
|
|
399
|
+
if image.registry and push:
|
|
400
|
+
command.append("--push")
|
|
401
|
+
command.append(tmp_dir)
|
|
402
|
+
|
|
403
|
+
concat_command = " ".join(command)
|
|
404
|
+
logger.debug(f"Build command: {concat_command}")
|
|
405
|
+
if dry_run:
|
|
406
|
+
click.secho("Dry run for docker builder...")
|
|
407
|
+
click.secho(f"Context path: {tmp_path}")
|
|
408
|
+
click.secho(f"Dockerfile: {dockerfile}")
|
|
409
|
+
click.secho(f"Command: {concat_command}")
|
|
410
|
+
return image.uri
|
|
411
|
+
else:
|
|
412
|
+
click.secho(f"Run command: {concat_command} ", fg="blue")
|
|
413
|
+
|
|
414
|
+
await asyncio.to_thread(subprocess.run, command, check=True)
|
|
415
|
+
|
|
416
|
+
return image.uri
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import typing
|
|
6
|
+
from typing import ClassVar, Dict, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from async_lru import alru_cache
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from typing_extensions import Protocol
|
|
11
|
+
|
|
12
|
+
from union._image import Architecture, Image
|
|
13
|
+
from union._logging import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ImageBuilder(Protocol):
|
|
17
|
+
async def build_image(self, image: Image, dry_run: bool) -> Optional[str]: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ImageChecker(Protocol):
|
|
21
|
+
@classmethod
|
|
22
|
+
async def image_exists(
|
|
23
|
+
cls, repository: str, tag: str, arch: Tuple[Architecture, ...] = ("linux/amd64",)
|
|
24
|
+
) -> bool: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DockerAPIImageChecker(ImageChecker):
|
|
28
|
+
"""
|
|
29
|
+
Unfortunately only works for docker hub as there's no way to get a public token for ghcr.io. See SO:
|
|
30
|
+
https://stackoverflow.com/questions/57316115/get-manifest-of-a-public-docker-image-hosted-on-docker-hub-using-the-docker-regi
|
|
31
|
+
The token used here seems to be short-lived (<1 second), so copy pasting doesn't even work.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
async def image_exists(cls, repository: str, tag: str, arch: Tuple[Architecture, ...] = ("linux/amd64",)) -> bool:
|
|
36
|
+
import httpx
|
|
37
|
+
|
|
38
|
+
if "/" in repository:
|
|
39
|
+
if not repository.startswith("library/"):
|
|
40
|
+
raise ValueError("This checker only works with Docker Hub")
|
|
41
|
+
else:
|
|
42
|
+
repository = f"library/{repository}"
|
|
43
|
+
|
|
44
|
+
auth_url = "https://auth.docker.io/token"
|
|
45
|
+
service = "registry.docker.io"
|
|
46
|
+
scope = f"repository:{repository}:pull"
|
|
47
|
+
|
|
48
|
+
async with httpx.AsyncClient() as client:
|
|
49
|
+
# Get auth token
|
|
50
|
+
auth_response = await client.get(auth_url, params={"service": service, "scope": scope})
|
|
51
|
+
if auth_response.status_code != 200:
|
|
52
|
+
raise Exception(f"Failed to get auth token: {auth_response.status_code}")
|
|
53
|
+
token = auth_response.json()["token"]
|
|
54
|
+
|
|
55
|
+
manifest_url = f"https://registry-1.docker.io/v2/{repository}/manifests/{tag}"
|
|
56
|
+
headers = {
|
|
57
|
+
"Authorization": f"Bearer {token}",
|
|
58
|
+
"Accept": (
|
|
59
|
+
"application/vnd.docker.distribution.manifest.v2+json,"
|
|
60
|
+
"application/vnd.docker.distribution.manifest.list.v2+json"
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
manifest_response = await client.get(manifest_url, headers=headers)
|
|
64
|
+
|
|
65
|
+
if manifest_response.status_code != 200:
|
|
66
|
+
raise Exception(f"Failed to get manifest: {manifest_response.status_code}")
|
|
67
|
+
manifest_list = manifest_response.json()["manifests"]
|
|
68
|
+
architectures = [f"{x['platform']['os']}/{x['platform']['architecture']}" for x in manifest_list]
|
|
69
|
+
architectures = set(architectures)
|
|
70
|
+
|
|
71
|
+
if architectures >= set(arch):
|
|
72
|
+
logger.debug(f"Image {repository}:{tag} found for architecture(s) {arch}, has {architectures}")
|
|
73
|
+
return True
|
|
74
|
+
else:
|
|
75
|
+
logger.debug(f"Image {repository}:{tag} not found for architecture(s) {arch}, only has {architectures}")
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class LocalDockerCommandImageChecker(ImageChecker):
|
|
80
|
+
command_name: ClassVar[str] = "docker"
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
async def image_exists(cls, repository: str, tag: str, arch: Tuple[Architecture, ...] = ("linux/amd64",)) -> bool:
|
|
84
|
+
# Check if the image exists locally by running the docker inspect command
|
|
85
|
+
process = await asyncio.create_subprocess_exec(
|
|
86
|
+
cls.command_name,
|
|
87
|
+
"manifest",
|
|
88
|
+
"inspect",
|
|
89
|
+
f"{repository}:{tag}",
|
|
90
|
+
stdout=asyncio.subprocess.PIPE,
|
|
91
|
+
stderr=asyncio.subprocess.PIPE,
|
|
92
|
+
)
|
|
93
|
+
stdout, stderr = await process.communicate()
|
|
94
|
+
if stderr and "manifest unknown" in stderr.decode():
|
|
95
|
+
logger.debug(f"Image {repository}:{tag} not found using the docker command.")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
if process.returncode != 0:
|
|
99
|
+
raise RuntimeError(f"Failed to run docker image inspect {repository}:{tag}")
|
|
100
|
+
|
|
101
|
+
inspect_data = json.loads(stdout.decode())
|
|
102
|
+
if "manifests" not in inspect_data:
|
|
103
|
+
raise RuntimeError(f"Invalid data returned from docker image inspect for {repository}:{tag}")
|
|
104
|
+
manifest_list = inspect_data["manifests"]
|
|
105
|
+
architectures = [f"{x['platform']['os']}/{x['platform']['architecture']}" for x in manifest_list]
|
|
106
|
+
architectures = set(architectures)
|
|
107
|
+
if architectures >= set(arch):
|
|
108
|
+
logger.debug(f"Image {repository}:{tag} found for architecture(s) {arch}, has {architectures}")
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
# Otherwise write a message and return false to trigger build
|
|
112
|
+
logger.debug(f"Image {repository}:{tag} not found for architecture(s) {arch}, only has {architectures}")
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class LocalPodmanCommandImageChecker(LocalDockerCommandImageChecker):
|
|
117
|
+
command_name: ClassVar[str] = "podman"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ImageBuildEngine:
|
|
121
|
+
"""
|
|
122
|
+
ImageBuildEngine contains a list of builders that can be used to build an ImageSpec.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
_REGISTRY: typing.ClassVar[typing.Dict[str, Tuple[ImageBuilder, int]]] = {}
|
|
126
|
+
_SEEN_IMAGES: typing.ClassVar[typing.Dict[str, str]] = {
|
|
127
|
+
# Set default for the auto container. See Image._identifier_override for more info.
|
|
128
|
+
"auto": Image.auto().uri,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def register(cls, builder_type: str, image_builder: ImageBuilder, priority: int = 5):
|
|
133
|
+
cls._REGISTRY[builder_type] = (image_builder, priority)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def get_registry(cls) -> Dict[str, Tuple[ImageBuilder, int]]:
|
|
137
|
+
return cls._REGISTRY
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
@alru_cache
|
|
141
|
+
async def image_exists(image: Image) -> bool:
|
|
142
|
+
if image.base_image is not None and not image._layers:
|
|
143
|
+
logger.debug(f"Image {image} has a base image: {image.base_image} and no layers. Skip existence check.")
|
|
144
|
+
return True
|
|
145
|
+
assert image.registry is not None, f"Image registry is not set for {image}"
|
|
146
|
+
assert image.name is not None, f"Image name is not set for {image}"
|
|
147
|
+
|
|
148
|
+
repository = image.registry + "/" + image.name
|
|
149
|
+
tag = image._final_tag
|
|
150
|
+
|
|
151
|
+
if tag == "latest":
|
|
152
|
+
logger.debug(f"Image {image} has tag 'latest', skip existence check, always build")
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
# Can get a public token for docker.io but ghcr requires a pat, so harder to get the manifest anonymously.
|
|
156
|
+
checkers = [LocalDockerCommandImageChecker, LocalPodmanCommandImageChecker, DockerAPIImageChecker]
|
|
157
|
+
for checker in checkers:
|
|
158
|
+
try:
|
|
159
|
+
exists = await checker.image_exists(repository, tag, set(image.platform))
|
|
160
|
+
logger.debug(f"Image {image} {exists=} in registry")
|
|
161
|
+
return exists
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.debug(f"Error checking image existence with {checker.__name__}: {e}")
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# If all checkers fail, then assume the image exists. This is current flytekit behavior
|
|
167
|
+
logger.info(f"All checkers failed to check existence of {image.uri}, assuming it does exists")
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
@alru_cache
|
|
172
|
+
async def build(
|
|
173
|
+
cls, image: Image, builder: Optional[str] = None, dry_run: bool = False, force: bool = False
|
|
174
|
+
) -> Optional[str]:
|
|
175
|
+
"""
|
|
176
|
+
Build the image. Images to be tagged with latest will always be built. Otherwise, this engine will check the
|
|
177
|
+
registry to see if the manifest exists.
|
|
178
|
+
|
|
179
|
+
:param image:
|
|
180
|
+
:param builder:
|
|
181
|
+
:param dry_run: Tell the builder to not actually build. Different builders will have different behaviors.
|
|
182
|
+
:param force: Skip the existence check. Normally if the image already exists we won't build it.
|
|
183
|
+
:return:
|
|
184
|
+
"""
|
|
185
|
+
# Always trigger a build if this is a dry run since builder shouldn't really do anything, or a force.
|
|
186
|
+
if force or dry_run or not await cls.image_exists(image):
|
|
187
|
+
logger.info(f"Image {image.uri} does not exist in registry or force/dry-run, building...")
|
|
188
|
+
|
|
189
|
+
# Validate the image before building
|
|
190
|
+
image.validate()
|
|
191
|
+
|
|
192
|
+
# If builder is not specified, use the first registered builder
|
|
193
|
+
img_builder = ImageBuildEngine._get_builder(builder)
|
|
194
|
+
|
|
195
|
+
result = await img_builder.build_image(image, dry_run=dry_run)
|
|
196
|
+
return result
|
|
197
|
+
else:
|
|
198
|
+
logger.info(f"Image {image.uri} already exists in registry. Skipping build.")
|
|
199
|
+
return image.uri
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def _get_builder(cls, builder: Optional[str]) -> ImageBuilder:
|
|
203
|
+
if not builder:
|
|
204
|
+
from .docker_builder import DockerImageBuilder
|
|
205
|
+
|
|
206
|
+
return DockerImageBuilder()
|
|
207
|
+
if builder not in cls._REGISTRY:
|
|
208
|
+
raise AssertionError(f"Image builder {builder} is not registered.")
|
|
209
|
+
return cls._REGISTRY[builder][0]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class ImageCache(BaseModel):
|
|
213
|
+
image_lookup: Dict[str, str]
|
|
214
|
+
serialized_form: str | None = None
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def to_transport(self) -> str:
|
|
218
|
+
"""
|
|
219
|
+
:return: returns the serialization context as a base64encoded, gzip compressed, json string
|
|
220
|
+
"""
|
|
221
|
+
# This is so that downstream tasks continue to have the same image lookup abilities
|
|
222
|
+
import base64
|
|
223
|
+
import gzip
|
|
224
|
+
from io import BytesIO
|
|
225
|
+
|
|
226
|
+
if self.serialized_form:
|
|
227
|
+
return self.serialized_form
|
|
228
|
+
json_str = self.model_dump_json(exclude={"serialized_form"})
|
|
229
|
+
buf = BytesIO()
|
|
230
|
+
with gzip.GzipFile(mode="wb", fileobj=buf, mtime=0) as f:
|
|
231
|
+
f.write(json_str.encode("utf-8"))
|
|
232
|
+
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
|
233
|
+
|
|
234
|
+
@classmethod
|
|
235
|
+
def from_transport(cls, s: str) -> ImageCache:
|
|
236
|
+
import base64
|
|
237
|
+
import gzip
|
|
238
|
+
|
|
239
|
+
compressed_val = base64.b64decode(s.encode("utf-8"))
|
|
240
|
+
json_str = gzip.decompress(compressed_val).decode("utf-8")
|
|
241
|
+
val = cls.model_validate_json(json_str)
|
|
242
|
+
val.serialized_form = s
|
|
243
|
+
return val
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import pathlib
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
from union._task import AsyncFunctionTaskTemplate, TaskTemplate
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_task_module(task: TaskTemplate) -> Tuple[str, str]:
|
|
10
|
+
"""
|
|
11
|
+
Extract the task module from the task template.
|
|
12
|
+
|
|
13
|
+
:param task: The task template to extract the module from.
|
|
14
|
+
:return: A tuple containing the entity name, module
|
|
15
|
+
"""
|
|
16
|
+
# TODO Work with current working directory, to figure out the module name, in case of nested launches
|
|
17
|
+
entity_name = task.name
|
|
18
|
+
if isinstance(task, AsyncFunctionTaskTemplate):
|
|
19
|
+
entity_module = inspect.getmodule(task.func).__name__
|
|
20
|
+
entity_name = task.func.__name__
|
|
21
|
+
else:
|
|
22
|
+
raise NotImplementedError(f"Task module {entity_name} not implemented.")
|
|
23
|
+
|
|
24
|
+
if entity_module == "__main__":
|
|
25
|
+
"""
|
|
26
|
+
This case is for the case in which the task is run from the main module.
|
|
27
|
+
"""
|
|
28
|
+
main_path = pathlib.Path(sys.modules["__main__"].__file__)
|
|
29
|
+
entity_module = main_path.stem
|
|
30
|
+
|
|
31
|
+
return entity_name, entity_module
|