flyte 0.1.0__py3-none-any.whl → 0.2.0a0__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.

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