flyte 2.0.0b32__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 +108 -0
- flyte/_bin/__init__.py +0 -0
- flyte/_bin/debug.py +38 -0
- flyte/_bin/runtime.py +195 -0
- flyte/_bin/serve.py +178 -0
- flyte/_build.py +26 -0
- flyte/_cache/__init__.py +12 -0
- flyte/_cache/cache.py +147 -0
- flyte/_cache/defaults.py +9 -0
- flyte/_cache/local_cache.py +216 -0
- flyte/_cache/policy_function_body.py +42 -0
- flyte/_code_bundle/__init__.py +8 -0
- flyte/_code_bundle/_ignore.py +121 -0
- flyte/_code_bundle/_packaging.py +218 -0
- flyte/_code_bundle/_utils.py +347 -0
- flyte/_code_bundle/bundle.py +266 -0
- flyte/_constants.py +1 -0
- flyte/_context.py +155 -0
- flyte/_custom_context.py +73 -0
- flyte/_debug/__init__.py +0 -0
- flyte/_debug/constants.py +38 -0
- flyte/_debug/utils.py +17 -0
- flyte/_debug/vscode.py +307 -0
- flyte/_deploy.py +408 -0
- flyte/_deployer.py +109 -0
- flyte/_doc.py +29 -0
- flyte/_docstring.py +32 -0
- flyte/_environment.py +122 -0
- flyte/_excepthook.py +37 -0
- flyte/_group.py +32 -0
- flyte/_hash.py +8 -0
- flyte/_image.py +1055 -0
- flyte/_initialize.py +628 -0
- flyte/_interface.py +119 -0
- flyte/_internal/__init__.py +3 -0
- flyte/_internal/controllers/__init__.py +129 -0
- flyte/_internal/controllers/_local_controller.py +239 -0
- flyte/_internal/controllers/_trace.py +48 -0
- flyte/_internal/controllers/remote/__init__.py +58 -0
- flyte/_internal/controllers/remote/_action.py +211 -0
- flyte/_internal/controllers/remote/_client.py +47 -0
- flyte/_internal/controllers/remote/_controller.py +583 -0
- flyte/_internal/controllers/remote/_core.py +465 -0
- flyte/_internal/controllers/remote/_informer.py +381 -0
- flyte/_internal/controllers/remote/_service_protocol.py +50 -0
- flyte/_internal/imagebuild/__init__.py +3 -0
- flyte/_internal/imagebuild/docker_builder.py +706 -0
- flyte/_internal/imagebuild/image_builder.py +277 -0
- flyte/_internal/imagebuild/remote_builder.py +386 -0
- flyte/_internal/imagebuild/utils.py +78 -0
- flyte/_internal/resolvers/__init__.py +0 -0
- flyte/_internal/resolvers/_task_module.py +21 -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 +486 -0
- flyte/_internal/runtime/entrypoints.py +204 -0
- flyte/_internal/runtime/io.py +188 -0
- flyte/_internal/runtime/resources_serde.py +152 -0
- flyte/_internal/runtime/reuse.py +125 -0
- flyte/_internal/runtime/rusty.py +193 -0
- flyte/_internal/runtime/task_serde.py +362 -0
- flyte/_internal/runtime/taskrunner.py +209 -0
- flyte/_internal/runtime/trigger_serde.py +160 -0
- flyte/_internal/runtime/types_serde.py +54 -0
- flyte/_keyring/__init__.py +0 -0
- flyte/_keyring/file.py +115 -0
- flyte/_logging.py +300 -0
- flyte/_map.py +312 -0
- flyte/_module.py +72 -0
- flyte/_pod.py +30 -0
- flyte/_resources.py +473 -0
- flyte/_retry.py +32 -0
- flyte/_reusable_environment.py +102 -0
- flyte/_run.py +724 -0
- flyte/_secret.py +96 -0
- flyte/_task.py +550 -0
- flyte/_task_environment.py +316 -0
- flyte/_task_plugins.py +47 -0
- flyte/_timeout.py +47 -0
- flyte/_tools.py +27 -0
- flyte/_trace.py +119 -0
- flyte/_trigger.py +1000 -0
- flyte/_utils/__init__.py +30 -0
- flyte/_utils/asyn.py +121 -0
- flyte/_utils/async_cache.py +139 -0
- flyte/_utils/coro_management.py +27 -0
- flyte/_utils/docker_credentials.py +173 -0
- flyte/_utils/file_handling.py +72 -0
- flyte/_utils/helpers.py +134 -0
- flyte/_utils/lazy_module.py +54 -0
- flyte/_utils/module_loader.py +104 -0
- flyte/_utils/org_discovery.py +57 -0
- flyte/_utils/uv_script_parser.py +49 -0
- flyte/_version.py +34 -0
- flyte/app/__init__.py +22 -0
- flyte/app/_app_environment.py +157 -0
- flyte/app/_deploy.py +125 -0
- flyte/app/_input.py +160 -0
- flyte/app/_runtime/__init__.py +3 -0
- flyte/app/_runtime/app_serde.py +347 -0
- flyte/app/_types.py +101 -0
- flyte/app/extras/__init__.py +3 -0
- flyte/app/extras/_fastapi.py +151 -0
- flyte/cli/__init__.py +12 -0
- flyte/cli/_abort.py +28 -0
- flyte/cli/_build.py +114 -0
- flyte/cli/_common.py +468 -0
- flyte/cli/_create.py +371 -0
- flyte/cli/_delete.py +45 -0
- flyte/cli/_deploy.py +293 -0
- flyte/cli/_gen.py +176 -0
- flyte/cli/_get.py +370 -0
- flyte/cli/_option.py +33 -0
- flyte/cli/_params.py +554 -0
- flyte/cli/_plugins.py +209 -0
- flyte/cli/_run.py +597 -0
- flyte/cli/_serve.py +64 -0
- flyte/cli/_update.py +37 -0
- flyte/cli/_user.py +17 -0
- flyte/cli/main.py +221 -0
- flyte/config/__init__.py +3 -0
- flyte/config/_config.py +248 -0
- flyte/config/_internal.py +73 -0
- flyte/config/_reader.py +225 -0
- flyte/connectors/__init__.py +11 -0
- flyte/connectors/_connector.py +270 -0
- flyte/connectors/_server.py +197 -0
- flyte/connectors/utils.py +135 -0
- flyte/errors.py +243 -0
- flyte/extend.py +19 -0
- flyte/extras/__init__.py +5 -0
- flyte/extras/_container.py +286 -0
- flyte/git/__init__.py +3 -0
- flyte/git/_config.py +21 -0
- flyte/io/__init__.py +29 -0
- flyte/io/_dataframe/__init__.py +131 -0
- flyte/io/_dataframe/basic_dfs.py +223 -0
- flyte/io/_dataframe/dataframe.py +1026 -0
- flyte/io/_dir.py +910 -0
- flyte/io/_file.py +914 -0
- flyte/io/_hashing_io.py +342 -0
- flyte/models.py +479 -0
- flyte/py.typed +0 -0
- flyte/remote/__init__.py +35 -0
- flyte/remote/_action.py +738 -0
- flyte/remote/_app.py +57 -0
- flyte/remote/_client/__init__.py +0 -0
- flyte/remote/_client/_protocols.py +189 -0
- flyte/remote/_client/auth/__init__.py +12 -0
- flyte/remote/_client/auth/_auth_utils.py +14 -0
- flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
- flyte/remote/_client/auth/_authenticators/base.py +403 -0
- flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
- flyte/remote/_client/auth/_authenticators/device_code.py +117 -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 +213 -0
- flyte/remote/_client/auth/_client_config.py +85 -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 +152 -0
- flyte/remote/_client/auth/_token_client.py +260 -0
- flyte/remote/_client/auth/errors.py +16 -0
- flyte/remote/_client/controlplane.py +128 -0
- flyte/remote/_common.py +30 -0
- flyte/remote/_console.py +19 -0
- flyte/remote/_data.py +161 -0
- flyte/remote/_logs.py +185 -0
- flyte/remote/_project.py +88 -0
- flyte/remote/_run.py +386 -0
- flyte/remote/_secret.py +142 -0
- flyte/remote/_task.py +527 -0
- flyte/remote/_trigger.py +306 -0
- flyte/remote/_user.py +33 -0
- flyte/report/__init__.py +3 -0
- flyte/report/_report.py +182 -0
- flyte/report/_template.html +124 -0
- flyte/storage/__init__.py +36 -0
- flyte/storage/_config.py +237 -0
- flyte/storage/_parallel_reader.py +274 -0
- flyte/storage/_remote_fs.py +34 -0
- flyte/storage/_storage.py +456 -0
- flyte/storage/_utils.py +5 -0
- flyte/syncify/__init__.py +56 -0
- flyte/syncify/_api.py +375 -0
- flyte/types/__init__.py +52 -0
- flyte/types/_interface.py +40 -0
- flyte/types/_pickle.py +145 -0
- flyte/types/_renderer.py +162 -0
- flyte/types/_string_literals.py +119 -0
- flyte/types/_type_engine.py +2254 -0
- flyte/types/_utils.py +80 -0
- flyte-2.0.0b32.data/scripts/debug.py +38 -0
- flyte-2.0.0b32.data/scripts/runtime.py +195 -0
- flyte-2.0.0b32.dist-info/METADATA +351 -0
- flyte-2.0.0b32.dist-info/RECORD +204 -0
- flyte-2.0.0b32.dist-info/WHEEL +5 -0
- flyte-2.0.0b32.dist-info/entry_points.txt +7 -0
- flyte-2.0.0b32.dist-info/licenses/LICENSE +201 -0
- flyte-2.0.0b32.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
import typing
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from string import Template
|
|
9
|
+
from typing import ClassVar, Optional, Protocol, cast
|
|
10
|
+
|
|
11
|
+
import aiofiles
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from flyte import Secret
|
|
15
|
+
from flyte._image import (
|
|
16
|
+
AptPackages,
|
|
17
|
+
Commands,
|
|
18
|
+
CopyConfig,
|
|
19
|
+
DockerIgnore,
|
|
20
|
+
Env,
|
|
21
|
+
Image,
|
|
22
|
+
Layer,
|
|
23
|
+
PipOption,
|
|
24
|
+
PipPackages,
|
|
25
|
+
PoetryProject,
|
|
26
|
+
PythonWheels,
|
|
27
|
+
Requirements,
|
|
28
|
+
UVProject,
|
|
29
|
+
UVScript,
|
|
30
|
+
WorkDir,
|
|
31
|
+
_DockerLines,
|
|
32
|
+
_ensure_tuple,
|
|
33
|
+
)
|
|
34
|
+
from flyte._internal.imagebuild.image_builder import (
|
|
35
|
+
DockerAPIImageChecker,
|
|
36
|
+
ImageBuilder,
|
|
37
|
+
ImageChecker,
|
|
38
|
+
LocalDockerCommandImageChecker,
|
|
39
|
+
LocalPodmanCommandImageChecker,
|
|
40
|
+
)
|
|
41
|
+
from flyte._internal.imagebuild.utils import copy_files_to_context, get_and_list_dockerignore
|
|
42
|
+
from flyte._logging import logger
|
|
43
|
+
|
|
44
|
+
_F_IMG_ID = "_F_IMG_ID"
|
|
45
|
+
FLYTE_DOCKER_BUILDER_CACHE_FROM = "FLYTE_DOCKER_BUILDER_CACHE_FROM"
|
|
46
|
+
FLYTE_DOCKER_BUILDER_CACHE_TO = "FLYTE_DOCKER_BUILDER_CACHE_TO"
|
|
47
|
+
|
|
48
|
+
UV_LOCK_WITHOUT_PROJECT_INSTALL_TEMPLATE = Template("""\
|
|
49
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
|
|
50
|
+
--mount=type=bind,target=uv.lock,src=$UV_LOCK_PATH,rw \
|
|
51
|
+
--mount=type=bind,target=pyproject.toml,src=$PYPROJECT_PATH \
|
|
52
|
+
$SECRET_MOUNT \
|
|
53
|
+
uv sync --active --inexact $PIP_INSTALL_ARGS
|
|
54
|
+
""")
|
|
55
|
+
|
|
56
|
+
UV_LOCK_INSTALL_TEMPLATE = Template("""\
|
|
57
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
|
|
58
|
+
--mount=type=bind,target=/root/.flyte/$PYPROJECT_PATH,src=$PYPROJECT_PATH,rw \
|
|
59
|
+
$SECRET_MOUNT \
|
|
60
|
+
uv sync --active --inexact --no-editable $PIP_INSTALL_ARGS --project /root/.flyte/$PYPROJECT_PATH
|
|
61
|
+
""")
|
|
62
|
+
|
|
63
|
+
POETRY_LOCK_WITHOUT_PROJECT_INSTALL_TEMPLATE = Template("""\
|
|
64
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
|
|
65
|
+
uv pip install poetry
|
|
66
|
+
|
|
67
|
+
ENV POETRY_CACHE_DIR=/tmp/poetry_cache \
|
|
68
|
+
POETRY_VIRTUALENVS_IN_PROJECT=true
|
|
69
|
+
|
|
70
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/tmp/poetry_cache,id=poetry \
|
|
71
|
+
--mount=type=bind,target=poetry.lock,src=$POETRY_LOCK_PATH \
|
|
72
|
+
--mount=type=bind,target=pyproject.toml,src=$PYPROJECT_PATH \
|
|
73
|
+
$SECRET_MOUNT \
|
|
74
|
+
poetry install $POETRY_INSTALL_ARGS
|
|
75
|
+
""")
|
|
76
|
+
|
|
77
|
+
POETRY_LOCK_INSTALL_TEMPLATE = Template("""\
|
|
78
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
|
|
79
|
+
uv pip install poetry
|
|
80
|
+
|
|
81
|
+
ENV POETRY_CACHE_DIR=/tmp/poetry_cache \
|
|
82
|
+
POETRY_VIRTUALENVS_IN_PROJECT=true
|
|
83
|
+
|
|
84
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/tmp/poetry_cache,id=poetry \
|
|
85
|
+
--mount=type=bind,target=/root/.flyte/$PYPROJECT_PATH,src=$PYPROJECT_PATH,rw \
|
|
86
|
+
$SECRET_MOUNT \
|
|
87
|
+
poetry install $POETRY_INSTALL_ARGS -C /root/.flyte/$PYPROJECT_PATH
|
|
88
|
+
""")
|
|
89
|
+
|
|
90
|
+
UV_PACKAGE_INSTALL_COMMAND_TEMPLATE = Template("""\
|
|
91
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
|
|
92
|
+
$REQUIREMENTS_MOUNT \
|
|
93
|
+
$SECRET_MOUNT \
|
|
94
|
+
uv pip install --python $$UV_PYTHON $PIP_INSTALL_ARGS
|
|
95
|
+
""")
|
|
96
|
+
|
|
97
|
+
UV_WHEEL_INSTALL_COMMAND_TEMPLATE = Template("""\
|
|
98
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=wheel \
|
|
99
|
+
--mount=source=/dist,target=/dist,type=bind \
|
|
100
|
+
$SECRET_MOUNT \
|
|
101
|
+
uv pip install --python $$UV_PYTHON $PIP_INSTALL_ARGS
|
|
102
|
+
""")
|
|
103
|
+
|
|
104
|
+
APT_INSTALL_COMMAND_TEMPLATE = Template("""\
|
|
105
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/var/cache/apt,id=apt \
|
|
106
|
+
$SECRET_MOUNT \
|
|
107
|
+
apt-get update && apt-get install -y --no-install-recommends \
|
|
108
|
+
$APT_PACKAGES
|
|
109
|
+
""")
|
|
110
|
+
|
|
111
|
+
UV_PYTHON_INSTALL_COMMAND = Template("""\
|
|
112
|
+
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
|
|
113
|
+
$SECRET_MOUNT \
|
|
114
|
+
uv pip install $PIP_INSTALL_ARGS
|
|
115
|
+
""")
|
|
116
|
+
|
|
117
|
+
# uv pip install --python /root/env/bin/python
|
|
118
|
+
# new template
|
|
119
|
+
DOCKER_FILE_UV_BASE_TEMPLATE = Template("""\
|
|
120
|
+
# syntax=docker/dockerfile:1.10
|
|
121
|
+
FROM ghcr.io/astral-sh/uv:0.8.13 AS uv
|
|
122
|
+
FROM $BASE_IMAGE
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
USER root
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# Copy in uv so that later commands don't have to mount it in
|
|
129
|
+
COPY --from=uv /uv /usr/bin/uv
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Configure default envs
|
|
133
|
+
ENV UV_COMPILE_BYTECODE=1 \
|
|
134
|
+
UV_LINK_MODE=copy \
|
|
135
|
+
VIRTUALENV=/opt/venv \
|
|
136
|
+
UV_PYTHON=/opt/venv/bin/python \
|
|
137
|
+
PATH="/opt/venv/bin:$$PATH"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Create a virtualenv with the user specified python version
|
|
141
|
+
RUN uv venv $$VIRTUALENV --python=$PYTHON_VERSION
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Adds nvidia just in case it exists
|
|
145
|
+
ENV PATH="$$PATH:/usr/local/nvidia/bin:/usr/local/cuda/bin" \
|
|
146
|
+
LD_LIBRARY_PATH="/usr/local/nvidia/lib64"
|
|
147
|
+
""")
|
|
148
|
+
|
|
149
|
+
# This gets added on to the end of the dockerfile
|
|
150
|
+
DOCKER_FILE_BASE_FOOTER = Template("""\
|
|
151
|
+
ENV _F_IMG_ID=$F_IMG_ID
|
|
152
|
+
WORKDIR /root
|
|
153
|
+
SHELL ["/bin/bash", "-c"]
|
|
154
|
+
""")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Handler(Protocol):
|
|
158
|
+
@staticmethod
|
|
159
|
+
async def handle(layer: Layer, context_path: Path, dockerfile: str) -> str: ...
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class PipAndRequirementsHandler:
|
|
163
|
+
@staticmethod
|
|
164
|
+
async def handle(layer: PipPackages, context_path: Path, dockerfile: str) -> str:
|
|
165
|
+
secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
|
|
166
|
+
|
|
167
|
+
# Set pip_install_args based on the layer type - either a requirements file or a list of packages
|
|
168
|
+
if isinstance(layer, Requirements):
|
|
169
|
+
if not layer.file.exists():
|
|
170
|
+
raise FileNotFoundError(f"Requirements file {layer.file} does not exist")
|
|
171
|
+
if not layer.file.is_file():
|
|
172
|
+
raise ValueError(f"Requirements file {layer.file} is not a file")
|
|
173
|
+
|
|
174
|
+
# Copy the requirements file to the context path
|
|
175
|
+
requirements_path = copy_files_to_context(layer.file, context_path)
|
|
176
|
+
rel_path = str(requirements_path.relative_to(context_path))
|
|
177
|
+
pip_install_args = layer.get_pip_install_args()
|
|
178
|
+
pip_install_args.extend(["--requirement", "requirements.txt"])
|
|
179
|
+
mount = f"--mount=type=bind,target=requirements.txt,src={rel_path}"
|
|
180
|
+
else:
|
|
181
|
+
mount = ""
|
|
182
|
+
requirements = list(layer.packages) if layer.packages else []
|
|
183
|
+
reqs = " ".join(requirements)
|
|
184
|
+
pip_install_args = layer.get_pip_install_args()
|
|
185
|
+
pip_install_args.append(reqs)
|
|
186
|
+
|
|
187
|
+
delta = UV_PACKAGE_INSTALL_COMMAND_TEMPLATE.substitute(
|
|
188
|
+
SECRET_MOUNT=secret_mounts,
|
|
189
|
+
REQUIREMENTS_MOUNT=mount,
|
|
190
|
+
PIP_INSTALL_ARGS=" ".join(pip_install_args),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
dockerfile += delta
|
|
194
|
+
|
|
195
|
+
return dockerfile
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class PythonWheelHandler:
|
|
199
|
+
@staticmethod
|
|
200
|
+
async def handle(layer: PythonWheels, context_path: Path, dockerfile: str) -> str:
|
|
201
|
+
shutil.copytree(layer.wheel_dir, context_path / "dist", dirs_exist_ok=True)
|
|
202
|
+
pip_install_args = layer.get_pip_install_args()
|
|
203
|
+
secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
|
|
204
|
+
|
|
205
|
+
# First install: Install the wheel without dependencies using --no-deps
|
|
206
|
+
pip_install_args_no_deps = [
|
|
207
|
+
*pip_install_args,
|
|
208
|
+
*[
|
|
209
|
+
"--find-links",
|
|
210
|
+
"/dist",
|
|
211
|
+
"--no-deps",
|
|
212
|
+
"--no-index",
|
|
213
|
+
"--reinstall",
|
|
214
|
+
layer.package_name,
|
|
215
|
+
],
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
delta1 = UV_WHEEL_INSTALL_COMMAND_TEMPLATE.substitute(
|
|
219
|
+
PIP_INSTALL_ARGS=" ".join(pip_install_args_no_deps), SECRET_MOUNT=secret_mounts
|
|
220
|
+
)
|
|
221
|
+
dockerfile += delta1
|
|
222
|
+
|
|
223
|
+
# Second install: Install dependencies from PyPI
|
|
224
|
+
pip_install_args_deps = [*pip_install_args, layer.package_name]
|
|
225
|
+
delta2 = UV_WHEEL_INSTALL_COMMAND_TEMPLATE.substitute(
|
|
226
|
+
PIP_INSTALL_ARGS=" ".join(pip_install_args_deps), SECRET_MOUNT=secret_mounts
|
|
227
|
+
)
|
|
228
|
+
dockerfile += delta2
|
|
229
|
+
|
|
230
|
+
return dockerfile
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class _DockerLinesHandler:
|
|
234
|
+
@staticmethod
|
|
235
|
+
async def handle(layer: _DockerLines, context_path: Path, dockerfile: str) -> str:
|
|
236
|
+
# Add the lines to the dockerfile
|
|
237
|
+
for line in layer.lines:
|
|
238
|
+
dockerfile += f"\n{line}\n"
|
|
239
|
+
|
|
240
|
+
return dockerfile
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class EnvHandler:
|
|
244
|
+
@staticmethod
|
|
245
|
+
async def handle(layer: Env, context_path: Path, dockerfile: str) -> str:
|
|
246
|
+
# Add the env vars to the dockerfile
|
|
247
|
+
for key, value in layer.env_vars:
|
|
248
|
+
dockerfile += f"\nENV {key}={value}\n"
|
|
249
|
+
|
|
250
|
+
return dockerfile
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class AptPackagesHandler:
|
|
254
|
+
@staticmethod
|
|
255
|
+
async def handle(layer: AptPackages, _: Path, dockerfile: str) -> str:
|
|
256
|
+
packages = layer.packages
|
|
257
|
+
secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
|
|
258
|
+
delta = APT_INSTALL_COMMAND_TEMPLATE.substitute(APT_PACKAGES=" ".join(packages), SECRET_MOUNT=secret_mounts)
|
|
259
|
+
dockerfile += delta
|
|
260
|
+
|
|
261
|
+
return dockerfile
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class UVProjectHandler:
|
|
265
|
+
@staticmethod
|
|
266
|
+
async def handle(
|
|
267
|
+
layer: UVProject, context_path: Path, dockerfile: str, docker_ignore_patterns: list[str] = []
|
|
268
|
+
) -> str:
|
|
269
|
+
secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
|
|
270
|
+
if layer.project_install_mode == "dependencies_only":
|
|
271
|
+
pip_install_args = " ".join(layer.get_pip_install_args())
|
|
272
|
+
if "--no-install-project" not in pip_install_args:
|
|
273
|
+
pip_install_args += " --no-install-project"
|
|
274
|
+
if "--no-sources" not in pip_install_args:
|
|
275
|
+
pip_install_args += " --no-sources"
|
|
276
|
+
# Only Copy pyproject.yaml and uv.lock.
|
|
277
|
+
pyproject_dst = copy_files_to_context(layer.pyproject, context_path)
|
|
278
|
+
uvlock_dst = copy_files_to_context(layer.uvlock, context_path)
|
|
279
|
+
delta = UV_LOCK_WITHOUT_PROJECT_INSTALL_TEMPLATE.substitute(
|
|
280
|
+
UV_LOCK_PATH=uvlock_dst.relative_to(context_path),
|
|
281
|
+
PYPROJECT_PATH=pyproject_dst.relative_to(context_path),
|
|
282
|
+
PIP_INSTALL_ARGS=pip_install_args,
|
|
283
|
+
SECRET_MOUNT=secret_mounts,
|
|
284
|
+
)
|
|
285
|
+
else:
|
|
286
|
+
# Copy the entire project.
|
|
287
|
+
pyproject_dst = copy_files_to_context(layer.pyproject.parent, context_path, docker_ignore_patterns)
|
|
288
|
+
|
|
289
|
+
# Make sure pyproject.toml and uv.lock files are not removed by docker ignore
|
|
290
|
+
uv_lock_context_path = pyproject_dst / "uv.lock"
|
|
291
|
+
pyproject_context_path = pyproject_dst / "pyproject.toml"
|
|
292
|
+
if not uv_lock_context_path.exists():
|
|
293
|
+
shutil.copy(layer.uvlock, pyproject_dst)
|
|
294
|
+
if not pyproject_context_path.exists():
|
|
295
|
+
shutil.copy(layer.pyproject, pyproject_dst)
|
|
296
|
+
|
|
297
|
+
delta = UV_LOCK_INSTALL_TEMPLATE.substitute(
|
|
298
|
+
PYPROJECT_PATH=pyproject_dst.relative_to(context_path),
|
|
299
|
+
PIP_INSTALL_ARGS=" ".join(layer.get_pip_install_args()),
|
|
300
|
+
SECRET_MOUNT=secret_mounts,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
dockerfile += delta
|
|
304
|
+
return dockerfile
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class PoetryProjectHandler:
|
|
308
|
+
@staticmethod
|
|
309
|
+
async def handel(
|
|
310
|
+
layer: PoetryProject, context_path: Path, dockerfile: str, docker_ignore_patterns: list[str] = []
|
|
311
|
+
) -> str:
|
|
312
|
+
secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
|
|
313
|
+
extra_args = layer.extra_args or ""
|
|
314
|
+
if layer.project_install_mode == "dependencies_only":
|
|
315
|
+
# Only Copy pyproject.yaml and poetry.lock.
|
|
316
|
+
pyproject_dst = copy_files_to_context(layer.pyproject, context_path)
|
|
317
|
+
poetry_lock_dst = copy_files_to_context(layer.poetry_lock, context_path)
|
|
318
|
+
if "--no-root" not in extra_args:
|
|
319
|
+
extra_args += " --no-root"
|
|
320
|
+
delta = POETRY_LOCK_WITHOUT_PROJECT_INSTALL_TEMPLATE.substitute(
|
|
321
|
+
POETRY_LOCK_PATH=poetry_lock_dst.relative_to(context_path),
|
|
322
|
+
PYPROJECT_PATH=pyproject_dst.relative_to(context_path),
|
|
323
|
+
POETRY_INSTALL_ARGS=extra_args,
|
|
324
|
+
SECRET_MOUNT=secret_mounts,
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
# Copy the entire project.
|
|
328
|
+
pyproject_dst = copy_files_to_context(layer.pyproject.parent, context_path, docker_ignore_patterns)
|
|
329
|
+
|
|
330
|
+
# Make sure pyproject.toml and poetry.lock files are not removed by docker ignore
|
|
331
|
+
poetry_lock_context_path = pyproject_dst / "poetry.lock"
|
|
332
|
+
pyproject_context_path = pyproject_dst / "pyproject.toml"
|
|
333
|
+
if not poetry_lock_context_path.exists():
|
|
334
|
+
shutil.copy(layer.poetry_lock, pyproject_dst)
|
|
335
|
+
if not pyproject_context_path.exists():
|
|
336
|
+
shutil.copy(layer.pyproject, pyproject_dst)
|
|
337
|
+
|
|
338
|
+
delta = POETRY_LOCK_INSTALL_TEMPLATE.substitute(
|
|
339
|
+
PYPROJECT_PATH=pyproject_dst.relative_to(context_path),
|
|
340
|
+
POETRY_INSTALL_ARGS=extra_args,
|
|
341
|
+
SECRET_MOUNT=secret_mounts,
|
|
342
|
+
)
|
|
343
|
+
dockerfile += delta
|
|
344
|
+
return dockerfile
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class DockerIgnoreHandler:
|
|
348
|
+
@staticmethod
|
|
349
|
+
async def handle(layer: DockerIgnore, context_path: Path, _: str):
|
|
350
|
+
shutil.copy(layer.path, context_path)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class CopyConfigHandler:
|
|
354
|
+
@staticmethod
|
|
355
|
+
async def handle(
|
|
356
|
+
layer: CopyConfig, context_path: Path, dockerfile: str, docker_ignore_patterns: list[str] = []
|
|
357
|
+
) -> str:
|
|
358
|
+
# Copy the source config file or directory to the context path
|
|
359
|
+
if layer.src.is_absolute() or ".." in str(layer.src):
|
|
360
|
+
dst_path = context_path / str(layer.src.absolute()).replace("/", "./_flyte_abs_context/", 1)
|
|
361
|
+
else:
|
|
362
|
+
dst_path = context_path / layer.src
|
|
363
|
+
|
|
364
|
+
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
365
|
+
abs_path = layer.src.absolute()
|
|
366
|
+
|
|
367
|
+
if layer.src.is_file():
|
|
368
|
+
# Copy the file
|
|
369
|
+
shutil.copy(abs_path, dst_path)
|
|
370
|
+
elif layer.src.is_dir():
|
|
371
|
+
# Copy the entire directory
|
|
372
|
+
shutil.copytree(
|
|
373
|
+
abs_path, dst_path, dirs_exist_ok=True, ignore=shutil.ignore_patterns(*docker_ignore_patterns)
|
|
374
|
+
)
|
|
375
|
+
else:
|
|
376
|
+
logger.error(f"Source path not exists: {layer.src}")
|
|
377
|
+
return dockerfile
|
|
378
|
+
|
|
379
|
+
# Add a copy command to the dockerfile
|
|
380
|
+
dockerfile += f"\nCOPY {dst_path.relative_to(context_path)} {layer.dst}\n"
|
|
381
|
+
return dockerfile
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class CommandsHandler:
|
|
385
|
+
@staticmethod
|
|
386
|
+
async def handle(layer: Commands, _: Path, dockerfile: str) -> str:
|
|
387
|
+
# Append raw commands to the dockerfile
|
|
388
|
+
secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
|
|
389
|
+
for command in layer.commands:
|
|
390
|
+
dockerfile += f"\nRUN {secret_mounts} {command}\n"
|
|
391
|
+
|
|
392
|
+
return dockerfile
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class WorkDirHandler:
|
|
396
|
+
@staticmethod
|
|
397
|
+
async def handle(layer: WorkDir, _: Path, dockerfile: str) -> str:
|
|
398
|
+
# cd to the workdir
|
|
399
|
+
dockerfile += f"\nWORKDIR {layer.workdir}\n"
|
|
400
|
+
|
|
401
|
+
return dockerfile
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _get_secret_commands(layers: typing.Tuple[Layer, ...]) -> typing.List[str]:
|
|
405
|
+
commands = []
|
|
406
|
+
|
|
407
|
+
def _get_secret_command(secret: str | Secret) -> typing.List[str]:
|
|
408
|
+
if isinstance(secret, str):
|
|
409
|
+
secret = Secret(key=secret)
|
|
410
|
+
secret_id = hash(secret)
|
|
411
|
+
secret_env_key = "_".join([k.upper() for k in filter(None, (secret.group, secret.key))])
|
|
412
|
+
if os.getenv(secret_env_key):
|
|
413
|
+
return ["--secret", f"id={secret_id},env={secret_env_key}"]
|
|
414
|
+
secret_file_name = "_".join(list(filter(None, (secret.group, secret.key))))
|
|
415
|
+
secret_file_path = f"/etc/secrets/{secret_file_name}"
|
|
416
|
+
if not os.path.exists(secret_file_path):
|
|
417
|
+
raise FileNotFoundError(f"Secret not found in Env Var {secret_env_key} or file {secret_file_path}")
|
|
418
|
+
return ["--secret", f"id={secret_id},src={secret_file_path}"]
|
|
419
|
+
|
|
420
|
+
for layer in layers:
|
|
421
|
+
if isinstance(layer, (PipOption, AptPackages, Commands)):
|
|
422
|
+
if layer.secret_mounts:
|
|
423
|
+
for secret_mount in layer.secret_mounts:
|
|
424
|
+
commands.extend(_get_secret_command(secret_mount))
|
|
425
|
+
return commands
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _get_secret_mounts_layer(secrets: typing.Tuple[str | Secret, ...] | None) -> str:
|
|
429
|
+
if secrets is None:
|
|
430
|
+
return ""
|
|
431
|
+
secret_mounts_layer = ""
|
|
432
|
+
for s in secrets:
|
|
433
|
+
secret = Secret(key=s) if isinstance(s, str) else s
|
|
434
|
+
secret_id = hash(secret)
|
|
435
|
+
if secret.mount:
|
|
436
|
+
secret_mounts_layer += f"--mount=type=secret,id={secret_id},target={secret.mount}"
|
|
437
|
+
elif secret.as_env_var:
|
|
438
|
+
secret_mounts_layer += f"--mount=type=secret,id={secret_id},env={secret.as_env_var}"
|
|
439
|
+
else:
|
|
440
|
+
secret_default_env_key = "_".join(list(filter(None, (secret.group, secret.key))))
|
|
441
|
+
secret_mounts_layer += f"--mount=type=secret,id={secret_id},env={secret_default_env_key}"
|
|
442
|
+
|
|
443
|
+
return secret_mounts_layer
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
async def _process_layer(
|
|
447
|
+
layer: Layer, context_path: Path, dockerfile: str, docker_ignore_patterns: list[str] = []
|
|
448
|
+
) -> str:
|
|
449
|
+
match layer:
|
|
450
|
+
case PythonWheels():
|
|
451
|
+
# Handle Python wheels
|
|
452
|
+
dockerfile = await PythonWheelHandler.handle(layer, context_path, dockerfile)
|
|
453
|
+
|
|
454
|
+
case UVScript():
|
|
455
|
+
# Handle UV script
|
|
456
|
+
from flyte._utils import parse_uv_script_file
|
|
457
|
+
|
|
458
|
+
header = parse_uv_script_file(layer.script)
|
|
459
|
+
if header.dependencies:
|
|
460
|
+
pip = PipPackages(
|
|
461
|
+
packages=_ensure_tuple(header.dependencies) if header.dependencies else None,
|
|
462
|
+
secret_mounts=layer.secret_mounts,
|
|
463
|
+
index_url=layer.index_url,
|
|
464
|
+
extra_args=layer.extra_args,
|
|
465
|
+
pre=layer.pre,
|
|
466
|
+
extra_index_urls=layer.extra_index_urls,
|
|
467
|
+
)
|
|
468
|
+
dockerfile = await PipAndRequirementsHandler.handle(pip, context_path, dockerfile)
|
|
469
|
+
|
|
470
|
+
case Requirements() | PipPackages():
|
|
471
|
+
# Handle pip packages and requirements
|
|
472
|
+
dockerfile = await PipAndRequirementsHandler.handle(layer, context_path, dockerfile)
|
|
473
|
+
|
|
474
|
+
case AptPackages():
|
|
475
|
+
# Handle apt packages
|
|
476
|
+
dockerfile = await AptPackagesHandler.handle(layer, context_path, dockerfile)
|
|
477
|
+
|
|
478
|
+
case UVProject():
|
|
479
|
+
# Handle UV project
|
|
480
|
+
dockerfile = await UVProjectHandler.handle(layer, context_path, dockerfile, docker_ignore_patterns)
|
|
481
|
+
|
|
482
|
+
case PoetryProject():
|
|
483
|
+
# Handle Poetry project
|
|
484
|
+
dockerfile = await PoetryProjectHandler.handel(layer, context_path, dockerfile, docker_ignore_patterns)
|
|
485
|
+
|
|
486
|
+
case PoetryProject():
|
|
487
|
+
# Handle Poetry project
|
|
488
|
+
dockerfile = await PoetryProjectHandler.handel(layer, context_path, dockerfile, docker_ignore_patterns)
|
|
489
|
+
|
|
490
|
+
case CopyConfig():
|
|
491
|
+
# Handle local files and folders
|
|
492
|
+
dockerfile = await CopyConfigHandler.handle(layer, context_path, dockerfile, docker_ignore_patterns)
|
|
493
|
+
|
|
494
|
+
case Commands():
|
|
495
|
+
# Handle commands
|
|
496
|
+
dockerfile = await CommandsHandler.handle(layer, context_path, dockerfile)
|
|
497
|
+
|
|
498
|
+
case DockerIgnore():
|
|
499
|
+
# Handle dockerignore
|
|
500
|
+
await DockerIgnoreHandler.handle(layer, context_path, dockerfile)
|
|
501
|
+
|
|
502
|
+
case WorkDir():
|
|
503
|
+
# Handle workdir
|
|
504
|
+
dockerfile = await WorkDirHandler.handle(layer, context_path, dockerfile)
|
|
505
|
+
|
|
506
|
+
case Env():
|
|
507
|
+
# Handle environment variables
|
|
508
|
+
dockerfile = await EnvHandler.handle(layer, context_path, dockerfile)
|
|
509
|
+
|
|
510
|
+
case _DockerLines():
|
|
511
|
+
# Only for internal use
|
|
512
|
+
dockerfile = await _DockerLinesHandler.handle(layer, context_path, dockerfile)
|
|
513
|
+
|
|
514
|
+
case _:
|
|
515
|
+
raise NotImplementedError(f"Layer type {type(layer)} not supported")
|
|
516
|
+
|
|
517
|
+
return dockerfile
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class DockerImageBuilder(ImageBuilder):
|
|
521
|
+
"""Image builder using Docker and buildkit."""
|
|
522
|
+
|
|
523
|
+
builder_type: ClassVar = "docker"
|
|
524
|
+
_builder_name: ClassVar = "flytex"
|
|
525
|
+
|
|
526
|
+
def get_checkers(self) -> Optional[typing.List[typing.Type[ImageChecker]]]:
|
|
527
|
+
# Can get a public token for docker.io but ghcr requires a pat, so harder to get the manifest anonymously
|
|
528
|
+
return [LocalDockerCommandImageChecker, LocalPodmanCommandImageChecker, DockerAPIImageChecker]
|
|
529
|
+
|
|
530
|
+
async def build_image(self, image: Image, dry_run: bool = False) -> str:
|
|
531
|
+
if image.dockerfile:
|
|
532
|
+
# If a dockerfile is provided, use it directly
|
|
533
|
+
return await self._build_from_dockerfile(image, push=True)
|
|
534
|
+
|
|
535
|
+
if len(image._layers) == 0:
|
|
536
|
+
logger.warning("No layers to build, returning the image URI as is.")
|
|
537
|
+
return image.uri
|
|
538
|
+
|
|
539
|
+
return await self._build_image(
|
|
540
|
+
image,
|
|
541
|
+
push=True,
|
|
542
|
+
dry_run=dry_run,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
async def _build_from_dockerfile(self, image: Image, push: bool) -> str:
|
|
546
|
+
"""
|
|
547
|
+
Build the image from a provided Dockerfile.
|
|
548
|
+
"""
|
|
549
|
+
assert image.dockerfile # for mypy
|
|
550
|
+
await DockerImageBuilder._ensure_buildx_builder()
|
|
551
|
+
|
|
552
|
+
command = [
|
|
553
|
+
"docker",
|
|
554
|
+
"buildx",
|
|
555
|
+
"build",
|
|
556
|
+
"--builder",
|
|
557
|
+
DockerImageBuilder._builder_name,
|
|
558
|
+
"-f",
|
|
559
|
+
str(image.dockerfile),
|
|
560
|
+
"--tag",
|
|
561
|
+
f"{image.uri}",
|
|
562
|
+
"--platform",
|
|
563
|
+
",".join(image.platform),
|
|
564
|
+
str(image.dockerfile.parent.absolute()), # Use the parent directory of the Dockerfile as the context
|
|
565
|
+
]
|
|
566
|
+
|
|
567
|
+
if image.registry and push:
|
|
568
|
+
command.append("--push")
|
|
569
|
+
else:
|
|
570
|
+
command.append("--load")
|
|
571
|
+
|
|
572
|
+
command.extend(_get_secret_commands(layers=image._layers))
|
|
573
|
+
|
|
574
|
+
concat_command = " ".join(command)
|
|
575
|
+
logger.debug(f"Build command: {concat_command}")
|
|
576
|
+
click.secho(f"Run command: {concat_command} ", fg="blue")
|
|
577
|
+
|
|
578
|
+
await asyncio.to_thread(subprocess.run, command, cwd=str(cast(Path, image.dockerfile).cwd()), check=True)
|
|
579
|
+
|
|
580
|
+
return image.uri
|
|
581
|
+
|
|
582
|
+
@staticmethod
|
|
583
|
+
async def _ensure_buildx_builder():
|
|
584
|
+
"""Ensure there is a docker buildx builder called flyte"""
|
|
585
|
+
# Check if buildx is available
|
|
586
|
+
try:
|
|
587
|
+
await asyncio.to_thread(
|
|
588
|
+
subprocess.run, ["docker", "buildx", "version"], check=True, stdout=subprocess.DEVNULL
|
|
589
|
+
)
|
|
590
|
+
except subprocess.CalledProcessError:
|
|
591
|
+
raise RuntimeError("Docker buildx is not available. Make sure BuildKit is installed and enabled.")
|
|
592
|
+
|
|
593
|
+
# List builders
|
|
594
|
+
result = await asyncio.to_thread(
|
|
595
|
+
subprocess.run, ["docker", "buildx", "ls"], capture_output=True, text=True, check=True
|
|
596
|
+
)
|
|
597
|
+
builders = result.stdout
|
|
598
|
+
|
|
599
|
+
# Check if there's any usable builder
|
|
600
|
+
if DockerImageBuilder._builder_name not in builders:
|
|
601
|
+
# No default builder found, create one
|
|
602
|
+
logger.info("No buildx builder found, creating one...")
|
|
603
|
+
await asyncio.to_thread(
|
|
604
|
+
subprocess.run,
|
|
605
|
+
[
|
|
606
|
+
"docker",
|
|
607
|
+
"buildx",
|
|
608
|
+
"create",
|
|
609
|
+
"--name",
|
|
610
|
+
DockerImageBuilder._builder_name,
|
|
611
|
+
"--platform",
|
|
612
|
+
"linux/amd64,linux/arm64",
|
|
613
|
+
],
|
|
614
|
+
check=True,
|
|
615
|
+
)
|
|
616
|
+
else:
|
|
617
|
+
logger.info("Buildx builder already exists.")
|
|
618
|
+
|
|
619
|
+
async def _build_image(self, image: Image, *, push: bool = True, dry_run: bool = False) -> str:
|
|
620
|
+
"""
|
|
621
|
+
if default image (only base image and locked), raise an error, don't have a dockerfile
|
|
622
|
+
if dockerfile, just build
|
|
623
|
+
in the main case, get the default Dockerfile template
|
|
624
|
+
- start from the base image
|
|
625
|
+
- use python to create a default venv and export variables
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
Then for the layers
|
|
629
|
+
- for each layer
|
|
630
|
+
- find the appropriate layer handler
|
|
631
|
+
- call layer handler with the context dir and the dockerfile
|
|
632
|
+
- handler can choose to do something (copy files from local) to the context and update the dockerfile
|
|
633
|
+
contents, returning the new string
|
|
634
|
+
"""
|
|
635
|
+
# For testing, set `push=False` to just build the image locally and not push to
|
|
636
|
+
# registry.
|
|
637
|
+
|
|
638
|
+
await DockerImageBuilder._ensure_buildx_builder()
|
|
639
|
+
|
|
640
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
641
|
+
logger.warning(f"Temporary directory: {tmp_dir}")
|
|
642
|
+
tmp_path = Path(tmp_dir)
|
|
643
|
+
|
|
644
|
+
dockerfile = DOCKER_FILE_UV_BASE_TEMPLATE.substitute(
|
|
645
|
+
BASE_IMAGE=image.base_image,
|
|
646
|
+
PYTHON_VERSION=f"{image.python_version[0]}.{image.python_version[1]}",
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
# Get .dockerignore file patterns first
|
|
650
|
+
docker_ignore_patterns = get_and_list_dockerignore(image)
|
|
651
|
+
|
|
652
|
+
for layer in image._layers:
|
|
653
|
+
dockerfile = await _process_layer(layer, tmp_path, dockerfile, docker_ignore_patterns)
|
|
654
|
+
|
|
655
|
+
dockerfile += DOCKER_FILE_BASE_FOOTER.substitute(F_IMG_ID=image.uri)
|
|
656
|
+
|
|
657
|
+
dockerfile_path = tmp_path / "Dockerfile"
|
|
658
|
+
async with aiofiles.open(dockerfile_path, mode="w") as f:
|
|
659
|
+
await f.write(dockerfile)
|
|
660
|
+
|
|
661
|
+
command = [
|
|
662
|
+
"docker",
|
|
663
|
+
"buildx",
|
|
664
|
+
"build",
|
|
665
|
+
"--builder",
|
|
666
|
+
DockerImageBuilder._builder_name,
|
|
667
|
+
"--tag",
|
|
668
|
+
f"{image.uri}",
|
|
669
|
+
"--platform",
|
|
670
|
+
",".join(image.platform),
|
|
671
|
+
]
|
|
672
|
+
|
|
673
|
+
cache_from = os.getenv(FLYTE_DOCKER_BUILDER_CACHE_FROM)
|
|
674
|
+
cache_to = os.getenv(FLYTE_DOCKER_BUILDER_CACHE_TO)
|
|
675
|
+
if cache_from and cache_to:
|
|
676
|
+
command[3:3] = [
|
|
677
|
+
f"--cache-from={cache_from}",
|
|
678
|
+
f"--cache-to={cache_to}",
|
|
679
|
+
]
|
|
680
|
+
|
|
681
|
+
if image.registry and push:
|
|
682
|
+
command.append("--push")
|
|
683
|
+
else:
|
|
684
|
+
command.append("--load")
|
|
685
|
+
|
|
686
|
+
command.extend(_get_secret_commands(layers=image._layers))
|
|
687
|
+
command.append(tmp_dir)
|
|
688
|
+
|
|
689
|
+
concat_command = " ".join(command)
|
|
690
|
+
logger.debug(f"Build command: {concat_command}")
|
|
691
|
+
if dry_run:
|
|
692
|
+
click.secho("Dry run for docker builder...")
|
|
693
|
+
click.secho(f"Context path: {tmp_path}")
|
|
694
|
+
click.secho(f"Dockerfile: {dockerfile}")
|
|
695
|
+
click.secho(f"Command: {concat_command}")
|
|
696
|
+
return image.uri
|
|
697
|
+
else:
|
|
698
|
+
click.secho(f"Run command: {concat_command} ", fg="blue")
|
|
699
|
+
|
|
700
|
+
try:
|
|
701
|
+
await asyncio.to_thread(subprocess.run, command, check=True)
|
|
702
|
+
except subprocess.CalledProcessError as e:
|
|
703
|
+
logger.error(f"Failed to build image: {e}")
|
|
704
|
+
raise RuntimeError(f"Failed to build image: {e}")
|
|
705
|
+
|
|
706
|
+
return image.uri
|