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.

Files changed (204) hide show
  1. flyte/__init__.py +108 -0
  2. flyte/_bin/__init__.py +0 -0
  3. flyte/_bin/debug.py +38 -0
  4. flyte/_bin/runtime.py +195 -0
  5. flyte/_bin/serve.py +178 -0
  6. flyte/_build.py +26 -0
  7. flyte/_cache/__init__.py +12 -0
  8. flyte/_cache/cache.py +147 -0
  9. flyte/_cache/defaults.py +9 -0
  10. flyte/_cache/local_cache.py +216 -0
  11. flyte/_cache/policy_function_body.py +42 -0
  12. flyte/_code_bundle/__init__.py +8 -0
  13. flyte/_code_bundle/_ignore.py +121 -0
  14. flyte/_code_bundle/_packaging.py +218 -0
  15. flyte/_code_bundle/_utils.py +347 -0
  16. flyte/_code_bundle/bundle.py +266 -0
  17. flyte/_constants.py +1 -0
  18. flyte/_context.py +155 -0
  19. flyte/_custom_context.py +73 -0
  20. flyte/_debug/__init__.py +0 -0
  21. flyte/_debug/constants.py +38 -0
  22. flyte/_debug/utils.py +17 -0
  23. flyte/_debug/vscode.py +307 -0
  24. flyte/_deploy.py +408 -0
  25. flyte/_deployer.py +109 -0
  26. flyte/_doc.py +29 -0
  27. flyte/_docstring.py +32 -0
  28. flyte/_environment.py +122 -0
  29. flyte/_excepthook.py +37 -0
  30. flyte/_group.py +32 -0
  31. flyte/_hash.py +8 -0
  32. flyte/_image.py +1055 -0
  33. flyte/_initialize.py +628 -0
  34. flyte/_interface.py +119 -0
  35. flyte/_internal/__init__.py +3 -0
  36. flyte/_internal/controllers/__init__.py +129 -0
  37. flyte/_internal/controllers/_local_controller.py +239 -0
  38. flyte/_internal/controllers/_trace.py +48 -0
  39. flyte/_internal/controllers/remote/__init__.py +58 -0
  40. flyte/_internal/controllers/remote/_action.py +211 -0
  41. flyte/_internal/controllers/remote/_client.py +47 -0
  42. flyte/_internal/controllers/remote/_controller.py +583 -0
  43. flyte/_internal/controllers/remote/_core.py +465 -0
  44. flyte/_internal/controllers/remote/_informer.py +381 -0
  45. flyte/_internal/controllers/remote/_service_protocol.py +50 -0
  46. flyte/_internal/imagebuild/__init__.py +3 -0
  47. flyte/_internal/imagebuild/docker_builder.py +706 -0
  48. flyte/_internal/imagebuild/image_builder.py +277 -0
  49. flyte/_internal/imagebuild/remote_builder.py +386 -0
  50. flyte/_internal/imagebuild/utils.py +78 -0
  51. flyte/_internal/resolvers/__init__.py +0 -0
  52. flyte/_internal/resolvers/_task_module.py +21 -0
  53. flyte/_internal/resolvers/common.py +31 -0
  54. flyte/_internal/resolvers/default.py +28 -0
  55. flyte/_internal/runtime/__init__.py +0 -0
  56. flyte/_internal/runtime/convert.py +486 -0
  57. flyte/_internal/runtime/entrypoints.py +204 -0
  58. flyte/_internal/runtime/io.py +188 -0
  59. flyte/_internal/runtime/resources_serde.py +152 -0
  60. flyte/_internal/runtime/reuse.py +125 -0
  61. flyte/_internal/runtime/rusty.py +193 -0
  62. flyte/_internal/runtime/task_serde.py +362 -0
  63. flyte/_internal/runtime/taskrunner.py +209 -0
  64. flyte/_internal/runtime/trigger_serde.py +160 -0
  65. flyte/_internal/runtime/types_serde.py +54 -0
  66. flyte/_keyring/__init__.py +0 -0
  67. flyte/_keyring/file.py +115 -0
  68. flyte/_logging.py +300 -0
  69. flyte/_map.py +312 -0
  70. flyte/_module.py +72 -0
  71. flyte/_pod.py +30 -0
  72. flyte/_resources.py +473 -0
  73. flyte/_retry.py +32 -0
  74. flyte/_reusable_environment.py +102 -0
  75. flyte/_run.py +724 -0
  76. flyte/_secret.py +96 -0
  77. flyte/_task.py +550 -0
  78. flyte/_task_environment.py +316 -0
  79. flyte/_task_plugins.py +47 -0
  80. flyte/_timeout.py +47 -0
  81. flyte/_tools.py +27 -0
  82. flyte/_trace.py +119 -0
  83. flyte/_trigger.py +1000 -0
  84. flyte/_utils/__init__.py +30 -0
  85. flyte/_utils/asyn.py +121 -0
  86. flyte/_utils/async_cache.py +139 -0
  87. flyte/_utils/coro_management.py +27 -0
  88. flyte/_utils/docker_credentials.py +173 -0
  89. flyte/_utils/file_handling.py +72 -0
  90. flyte/_utils/helpers.py +134 -0
  91. flyte/_utils/lazy_module.py +54 -0
  92. flyte/_utils/module_loader.py +104 -0
  93. flyte/_utils/org_discovery.py +57 -0
  94. flyte/_utils/uv_script_parser.py +49 -0
  95. flyte/_version.py +34 -0
  96. flyte/app/__init__.py +22 -0
  97. flyte/app/_app_environment.py +157 -0
  98. flyte/app/_deploy.py +125 -0
  99. flyte/app/_input.py +160 -0
  100. flyte/app/_runtime/__init__.py +3 -0
  101. flyte/app/_runtime/app_serde.py +347 -0
  102. flyte/app/_types.py +101 -0
  103. flyte/app/extras/__init__.py +3 -0
  104. flyte/app/extras/_fastapi.py +151 -0
  105. flyte/cli/__init__.py +12 -0
  106. flyte/cli/_abort.py +28 -0
  107. flyte/cli/_build.py +114 -0
  108. flyte/cli/_common.py +468 -0
  109. flyte/cli/_create.py +371 -0
  110. flyte/cli/_delete.py +45 -0
  111. flyte/cli/_deploy.py +293 -0
  112. flyte/cli/_gen.py +176 -0
  113. flyte/cli/_get.py +370 -0
  114. flyte/cli/_option.py +33 -0
  115. flyte/cli/_params.py +554 -0
  116. flyte/cli/_plugins.py +209 -0
  117. flyte/cli/_run.py +597 -0
  118. flyte/cli/_serve.py +64 -0
  119. flyte/cli/_update.py +37 -0
  120. flyte/cli/_user.py +17 -0
  121. flyte/cli/main.py +221 -0
  122. flyte/config/__init__.py +3 -0
  123. flyte/config/_config.py +248 -0
  124. flyte/config/_internal.py +73 -0
  125. flyte/config/_reader.py +225 -0
  126. flyte/connectors/__init__.py +11 -0
  127. flyte/connectors/_connector.py +270 -0
  128. flyte/connectors/_server.py +197 -0
  129. flyte/connectors/utils.py +135 -0
  130. flyte/errors.py +243 -0
  131. flyte/extend.py +19 -0
  132. flyte/extras/__init__.py +5 -0
  133. flyte/extras/_container.py +286 -0
  134. flyte/git/__init__.py +3 -0
  135. flyte/git/_config.py +21 -0
  136. flyte/io/__init__.py +29 -0
  137. flyte/io/_dataframe/__init__.py +131 -0
  138. flyte/io/_dataframe/basic_dfs.py +223 -0
  139. flyte/io/_dataframe/dataframe.py +1026 -0
  140. flyte/io/_dir.py +910 -0
  141. flyte/io/_file.py +914 -0
  142. flyte/io/_hashing_io.py +342 -0
  143. flyte/models.py +479 -0
  144. flyte/py.typed +0 -0
  145. flyte/remote/__init__.py +35 -0
  146. flyte/remote/_action.py +738 -0
  147. flyte/remote/_app.py +57 -0
  148. flyte/remote/_client/__init__.py +0 -0
  149. flyte/remote/_client/_protocols.py +189 -0
  150. flyte/remote/_client/auth/__init__.py +12 -0
  151. flyte/remote/_client/auth/_auth_utils.py +14 -0
  152. flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
  153. flyte/remote/_client/auth/_authenticators/base.py +403 -0
  154. flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
  155. flyte/remote/_client/auth/_authenticators/device_code.py +117 -0
  156. flyte/remote/_client/auth/_authenticators/external_command.py +79 -0
  157. flyte/remote/_client/auth/_authenticators/factory.py +200 -0
  158. flyte/remote/_client/auth/_authenticators/pkce.py +516 -0
  159. flyte/remote/_client/auth/_channel.py +213 -0
  160. flyte/remote/_client/auth/_client_config.py +85 -0
  161. flyte/remote/_client/auth/_default_html.py +32 -0
  162. flyte/remote/_client/auth/_grpc_utils/__init__.py +0 -0
  163. flyte/remote/_client/auth/_grpc_utils/auth_interceptor.py +288 -0
  164. flyte/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +151 -0
  165. flyte/remote/_client/auth/_keyring.py +152 -0
  166. flyte/remote/_client/auth/_token_client.py +260 -0
  167. flyte/remote/_client/auth/errors.py +16 -0
  168. flyte/remote/_client/controlplane.py +128 -0
  169. flyte/remote/_common.py +30 -0
  170. flyte/remote/_console.py +19 -0
  171. flyte/remote/_data.py +161 -0
  172. flyte/remote/_logs.py +185 -0
  173. flyte/remote/_project.py +88 -0
  174. flyte/remote/_run.py +386 -0
  175. flyte/remote/_secret.py +142 -0
  176. flyte/remote/_task.py +527 -0
  177. flyte/remote/_trigger.py +306 -0
  178. flyte/remote/_user.py +33 -0
  179. flyte/report/__init__.py +3 -0
  180. flyte/report/_report.py +182 -0
  181. flyte/report/_template.html +124 -0
  182. flyte/storage/__init__.py +36 -0
  183. flyte/storage/_config.py +237 -0
  184. flyte/storage/_parallel_reader.py +274 -0
  185. flyte/storage/_remote_fs.py +34 -0
  186. flyte/storage/_storage.py +456 -0
  187. flyte/storage/_utils.py +5 -0
  188. flyte/syncify/__init__.py +56 -0
  189. flyte/syncify/_api.py +375 -0
  190. flyte/types/__init__.py +52 -0
  191. flyte/types/_interface.py +40 -0
  192. flyte/types/_pickle.py +145 -0
  193. flyte/types/_renderer.py +162 -0
  194. flyte/types/_string_literals.py +119 -0
  195. flyte/types/_type_engine.py +2254 -0
  196. flyte/types/_utils.py +80 -0
  197. flyte-2.0.0b32.data/scripts/debug.py +38 -0
  198. flyte-2.0.0b32.data/scripts/runtime.py +195 -0
  199. flyte-2.0.0b32.dist-info/METADATA +351 -0
  200. flyte-2.0.0b32.dist-info/RECORD +204 -0
  201. flyte-2.0.0b32.dist-info/WHEEL +5 -0
  202. flyte-2.0.0b32.dist-info/entry_points.txt +7 -0
  203. flyte-2.0.0b32.dist-info/licenses/LICENSE +201 -0
  204. flyte-2.0.0b32.dist-info/top_level.txt +1 -0
@@ -0,0 +1,277 @@
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._initialize import _get_init_config
14
+ from flyte._logging import logger
15
+
16
+
17
+ class ImageBuilder(Protocol):
18
+ async def build_image(self, image: Image, dry_run: bool) -> str: ...
19
+
20
+ def get_checkers(self) -> Optional[typing.List[typing.Type[ImageChecker]]]:
21
+ """
22
+ Returns ImageCheckers that can be used to check if the image exists in the registry.
23
+ If None, then use the default checkers.
24
+ """
25
+ return None
26
+
27
+
28
+ class ImageChecker(Protocol):
29
+ @classmethod
30
+ async def image_exists(
31
+ cls, repository: str, tag: str, arch: Tuple[Architecture, ...] = ("linux/amd64",)
32
+ ) -> Optional[str]: ...
33
+
34
+
35
+ class DockerAPIImageChecker(ImageChecker):
36
+ """
37
+ Unfortunately only works for docker hub as there's no way to get a public token for ghcr.io. See SO:
38
+ https://stackoverflow.com/questions/57316115/get-manifest-of-a-public-docker-image-hosted-on-docker-hub-using-the-docker-regi
39
+ The token used here seems to be short-lived (<1 second), so copy pasting doesn't even work.
40
+ """
41
+
42
+ @classmethod
43
+ async def image_exists(
44
+ cls, repository: str, tag: str, arch: Tuple[Architecture, ...] = ("linux/amd64",)
45
+ ) -> Optional[str]:
46
+ import httpx
47
+
48
+ if "/" not in repository:
49
+ repository = f"library/{repository}"
50
+
51
+ auth_url = "https://auth.docker.io/token"
52
+ service = "registry.docker.io"
53
+ scope = f"repository:{repository}:pull"
54
+
55
+ async with httpx.AsyncClient() as client:
56
+ # Get auth token
57
+ auth_response = await client.get(auth_url, params={"service": service, "scope": scope})
58
+ if auth_response.status_code != 200:
59
+ raise Exception(f"Failed to get auth token: {auth_response.status_code}")
60
+
61
+ token = auth_response.json()["token"]
62
+
63
+ # ghcr.io/union-oss/flyte:latest
64
+ manifest_url = f"https://registry-1.docker.io/v2/{repository}/manifests/{tag}"
65
+ headers = {
66
+ "Authorization": f"Bearer {token}",
67
+ "Accept": (
68
+ "application/vnd.docker.distribution.manifest.v2+json,"
69
+ "application/vnd.docker.distribution.manifest.list.v2+json"
70
+ ),
71
+ }
72
+
73
+ manifest_response = await client.get(manifest_url, headers=headers)
74
+ if manifest_response.status_code != 200:
75
+ logger.warning(f"Image not found: {repository}:{tag} (HTTP {manifest_response.status_code})")
76
+ return None
77
+
78
+ manifest_list = manifest_response.json()["manifests"]
79
+ architectures = [f"{m['platform']['os']}/{m['platform']['architecture']}" for m in manifest_list]
80
+
81
+ if set(arch).issubset(set(architectures)):
82
+ logger.debug(f"Image {repository}:{tag} found with arch {architectures}")
83
+ return f"{repository}:{tag}"
84
+ else:
85
+ logger.debug(f"Image {repository}:{tag} has {architectures}, but missing {arch}")
86
+ return None
87
+
88
+
89
+ class LocalDockerCommandImageChecker(ImageChecker):
90
+ command_name: ClassVar[str] = "docker"
91
+
92
+ @classmethod
93
+ async def image_exists(
94
+ cls, repository: str, tag: str, arch: Tuple[Architecture, ...] = ("linux/amd64",)
95
+ ) -> Optional[str]:
96
+ # Check if the image exists locally by running the docker inspect command
97
+ process = await asyncio.create_subprocess_exec(
98
+ cls.command_name,
99
+ "manifest",
100
+ "inspect",
101
+ f"{repository}:{tag}",
102
+ stdout=asyncio.subprocess.PIPE,
103
+ stderr=asyncio.subprocess.PIPE,
104
+ )
105
+ stdout, stderr = await process.communicate()
106
+ if (stderr and "manifest unknown") or "no such manifest" in stderr.decode():
107
+ logger.debug(f"Image {repository}:{tag} not found using the docker command.")
108
+ return None
109
+
110
+ if process.returncode != 0:
111
+ raise RuntimeError(f"Failed to run docker image inspect {repository}:{tag}")
112
+
113
+ inspect_data = json.loads(stdout.decode())
114
+ if "manifests" not in inspect_data:
115
+ raise RuntimeError(f"Invalid data returned from docker image inspect for {repository}:{tag}")
116
+ manifest_list = inspect_data["manifests"]
117
+ architectures = [f"{x['platform']['os']}/{x['platform']['architecture']}" for x in manifest_list]
118
+ if set(architectures) >= set(arch):
119
+ logger.debug(f"Image {repository}:{tag} found for architecture(s) {arch}, has {architectures}")
120
+ return f"{repository}:{tag}"
121
+
122
+ # Otherwise write a message and return false to trigger build
123
+ logger.debug(f"Image {repository}:{tag} not found for architecture(s) {arch}, only has {architectures}")
124
+ return None
125
+
126
+
127
+ class LocalPodmanCommandImageChecker(LocalDockerCommandImageChecker):
128
+ command_name: ClassVar[str] = "podman"
129
+
130
+
131
+ class ImageBuildEngine:
132
+ """
133
+ ImageBuildEngine contains a list of builders that can be used to build an ImageSpec.
134
+ """
135
+
136
+ ImageBuilderType = typing.Literal["local", "remote"]
137
+
138
+ @staticmethod
139
+ @alru_cache
140
+ async def image_exists(image: Image) -> Optional[str]:
141
+ if image.base_image is not None and not image._layers:
142
+ logger.debug(f"Image {image} has a base image: {image.base_image} and no layers. Skip existence check.")
143
+ return image.uri
144
+ assert image.name is not None, f"Image name is not set for {image}"
145
+
146
+ tag = image._final_tag
147
+
148
+ if tag == "latest":
149
+ logger.debug(f"Image {image} has tag 'latest', skip existence check, always build")
150
+ return image.uri
151
+
152
+ builder = None
153
+ cfg = _get_init_config()
154
+ if cfg and cfg.image_builder:
155
+ builder = cfg.image_builder
156
+ image_builder = ImageBuildEngine._get_builder(builder)
157
+ image_checker = image_builder.get_checkers()
158
+ if image_checker is None:
159
+ logger.info(f"No image checkers found for builder `{image_builder}`, assuming it exists")
160
+ return image.uri
161
+ for checker in image_checker:
162
+ try:
163
+ repository = image.registry + "/" + image.name if image.registry else image.name
164
+ image_uri = await checker.image_exists(repository, tag, tuple(image.platform))
165
+ if image_uri:
166
+ logger.debug(f"Image {image_uri} in registry")
167
+ return image_uri
168
+ except Exception as e:
169
+ logger.debug(f"Error checking image existence with {checker.__name__}: {e}")
170
+ continue
171
+
172
+ # If all checkers fail, then assume the image exists. This is current flytekit behavior
173
+ logger.info(f"All checkers failed to check existence of {image.uri}, assuming it does exists")
174
+ return image.uri
175
+
176
+ @classmethod
177
+ @alru_cache
178
+ async def build(
179
+ cls,
180
+ image: Image,
181
+ builder: ImageBuildEngine.ImageBuilderType | None = None,
182
+ dry_run: bool = False,
183
+ force: bool = False,
184
+ ) -> str:
185
+ """
186
+ Build the image. Images to be tagged with latest will always be built. Otherwise, this engine will check the
187
+ registry to see if the manifest exists.
188
+
189
+ :param image:
190
+ :param builder:
191
+ :param dry_run: Tell the builder to not actually build. Different builders will have different behaviors.
192
+ :param force: Skip the existence check. Normally if the image already exists we won't build it.
193
+ :return:
194
+ """
195
+ # Always trigger a build if this is a dry run since builder shouldn't really do anything, or a force.
196
+ image_uri = (await cls.image_exists(image)) or image.uri
197
+ if force or dry_run or not await cls.image_exists(image):
198
+ logger.info(f"Image {image_uri} does not exist in registry or force/dry-run, building...")
199
+
200
+ # Validate the image before building
201
+ image.validate()
202
+
203
+ # If a builder is not specified, use the first registered builder
204
+ cfg = _get_init_config()
205
+ if cfg and cfg.image_builder:
206
+ builder = builder or cfg.image_builder
207
+ img_builder = ImageBuildEngine._get_builder(builder)
208
+ logger.debug(f"Using `{img_builder}` image builder to build image.")
209
+
210
+ result = await img_builder.build_image(image, dry_run=dry_run)
211
+ return result
212
+ else:
213
+ logger.info(f"Image {image_uri} already exists in registry. Skipping build.")
214
+ return image_uri
215
+
216
+ @classmethod
217
+ def _get_builder(cls, builder: ImageBuildEngine.ImageBuilderType | None = "local") -> ImageBuilder:
218
+ if builder is None:
219
+ builder = "local"
220
+ if builder == "remote":
221
+ from flyte._internal.imagebuild.remote_builder import RemoteImageBuilder
222
+
223
+ return RemoteImageBuilder()
224
+ elif builder == "local":
225
+ from flyte._internal.imagebuild.docker_builder import DockerImageBuilder
226
+
227
+ return DockerImageBuilder()
228
+ else:
229
+ raise ValueError(f"Unknown image builder type: {builder}. Supported types are 'local' and 'remote'.")
230
+
231
+
232
+ class ImageCache(BaseModel):
233
+ image_lookup: Dict[str, str]
234
+ serialized_form: str | None = None
235
+
236
+ @property
237
+ def to_transport(self) -> str:
238
+ """
239
+ :return: returns the serialization context as a base64encoded, gzip compressed, json string
240
+ """
241
+ # This is so that downstream tasks continue to have the same image lookup abilities
242
+ import base64
243
+ import gzip
244
+ from io import BytesIO
245
+
246
+ if self.serialized_form:
247
+ return self.serialized_form
248
+ json_str = self.model_dump_json(exclude={"serialized_form"})
249
+ buf = BytesIO()
250
+ with gzip.GzipFile(mode="wb", fileobj=buf, mtime=0) as f:
251
+ f.write(json_str.encode("utf-8"))
252
+ return base64.b64encode(buf.getvalue()).decode("utf-8")
253
+
254
+ @classmethod
255
+ def from_transport(cls, s: str) -> ImageCache:
256
+ import base64
257
+ import gzip
258
+
259
+ compressed_val = base64.b64decode(s.encode("utf-8"))
260
+ json_str = gzip.decompress(compressed_val).decode("utf-8")
261
+ val = cls.model_validate_json(json_str)
262
+ val.serialized_form = s
263
+ return val
264
+
265
+ def repr(self) -> typing.List[typing.List[Tuple[str, str]]]:
266
+ """
267
+ Returns a detailed representation of the deployed environments.
268
+ """
269
+ tuples = []
270
+ for k, v in self.image_lookup.items():
271
+ tuples.append(
272
+ [
273
+ ("Name", k),
274
+ ("image", v),
275
+ ]
276
+ )
277
+ return tuples
@@ -0,0 +1,386 @@
1
+ import gzip
2
+ import os
3
+ import shutil
4
+ import tarfile
5
+ import tempfile
6
+ import typing
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Optional, Tuple, cast
10
+ from uuid import uuid4
11
+
12
+ import aiofiles
13
+
14
+ import flyte
15
+ import flyte.errors
16
+ from flyte import Image, remote
17
+ from flyte._code_bundle._utils import tar_strip_file_attributes
18
+ from flyte._image import (
19
+ _BASE_REGISTRY,
20
+ AptPackages,
21
+ Architecture,
22
+ Commands,
23
+ CopyConfig,
24
+ DockerIgnore,
25
+ Env,
26
+ PipOption,
27
+ PipPackages,
28
+ PoetryProject,
29
+ PythonWheels,
30
+ Requirements,
31
+ UVProject,
32
+ UVScript,
33
+ WorkDir,
34
+ )
35
+ from flyte._internal.imagebuild.image_builder import ImageBuilder, ImageChecker
36
+ from flyte._internal.imagebuild.utils import copy_files_to_context, get_and_list_dockerignore
37
+ from flyte._internal.runtime.task_serde import get_security_context
38
+ from flyte._logging import logger
39
+ from flyte._secret import Secret
40
+ from flyte.remote import ActionOutputs, Run
41
+
42
+ if TYPE_CHECKING:
43
+ from flyteidl2.imagebuilder import definition_pb2 as image_definition_pb2
44
+
45
+ IMAGE_TASK_NAME = "build-image"
46
+ IMAGE_TASK_PROJECT = "system"
47
+ IMAGE_TASK_DOMAIN = "production"
48
+
49
+
50
+ class RemoteImageChecker(ImageChecker):
51
+ _images_client = None
52
+
53
+ @classmethod
54
+ async def image_exists(
55
+ cls, repository: str, tag: str, arch: Tuple[Architecture, ...] = ("linux/amd64",)
56
+ ) -> Optional[str]:
57
+ try:
58
+ import flyte.remote as remote
59
+
60
+ remote.Task.get(
61
+ name=IMAGE_TASK_NAME,
62
+ project=IMAGE_TASK_PROJECT,
63
+ domain=IMAGE_TASK_DOMAIN,
64
+ auto_version="latest",
65
+ )
66
+ except Exception as e:
67
+ msg = "remote image builder is not enabled. Please contact Union support to enable it."
68
+ raise flyte.errors.ImageBuildError(msg) from e
69
+
70
+ image_name = f"{repository.split('/')[-1]}:{tag}"
71
+
72
+ try:
73
+ from flyteidl2.common.identifier_pb2 import ProjectIdentifier
74
+ from flyteidl2.imagebuilder import definition_pb2 as image_definition__pb2
75
+ from flyteidl2.imagebuilder import payload_pb2 as image_payload__pb2
76
+ from flyteidl2.imagebuilder import service_pb2_grpc as image_service_pb2_grpc
77
+
78
+ from flyte._initialize import _get_init_config
79
+
80
+ cfg = _get_init_config()
81
+ if cfg is None:
82
+ raise ValueError("Init config should not be None")
83
+ image_id = image_definition__pb2.ImageIdentifier(name=image_name)
84
+ req = image_payload__pb2.GetImageRequest(
85
+ id=image_id,
86
+ organization=cfg.org,
87
+ project_id=ProjectIdentifier(organization=cfg.org, domain=cfg.domain, name=cfg.project),
88
+ )
89
+ if cls._images_client is None:
90
+ if cfg.client is None:
91
+ raise ValueError("remote client should not be None")
92
+ cls._images_client = image_service_pb2_grpc.ImageServiceStub(cfg.client._channel)
93
+ resp = await cls._images_client.GetImage(req)
94
+ logger.warning(f"[blue]Image {resp.image.fqin} found. Skip building.[/blue]")
95
+ return resp.image.fqin
96
+ except Exception:
97
+ logger.warning(f"[blue]Image {image_name} was not found or has expired.[/blue]", extra={"highlight": False})
98
+ return None
99
+
100
+
101
+ class RemoteImageBuilder(ImageBuilder):
102
+ def get_checkers(self) -> Optional[typing.List[typing.Type[ImageChecker]]]:
103
+ """Return the image checker."""
104
+ return [RemoteImageChecker]
105
+
106
+ async def build_image(self, image: Image, dry_run: bool = False) -> str:
107
+ from flyteidl2.workflow import run_definition_pb2
108
+
109
+ image_name = f"{image.name}:{image._final_tag}"
110
+ spec, context = await _validate_configuration(image)
111
+
112
+ start = datetime.now(timezone.utc)
113
+ entity = await remote.Task.get(
114
+ name=IMAGE_TASK_NAME,
115
+ project=IMAGE_TASK_PROJECT,
116
+ domain=IMAGE_TASK_DOMAIN,
117
+ auto_version="latest",
118
+ ).override.aio(secrets=_get_build_secrets_from_image(image))
119
+
120
+ logger.warning("[bold blue]🐳 Submitting a new build...[/bold blue]")
121
+ if image.registry and image.registry != _BASE_REGISTRY:
122
+ target_image = f"{image.registry}/{image_name}"
123
+ else:
124
+ # Use the default system registry in the backend.
125
+ target_image = image_name
126
+
127
+ from flyte._initialize import get_init_config
128
+
129
+ cfg = get_init_config()
130
+ run = cast(
131
+ Run,
132
+ await flyte.with_runcontext(
133
+ project=cfg.project, domain=cfg.domain, cache_lookup_scope="project-domain"
134
+ ).run.aio(entity, spec=spec, context=context, target_image=target_image),
135
+ )
136
+ logger.warning(f"⏳ Waiting for build to finish at: [bold cyan link={run.url}]{run.url}[/bold cyan link]")
137
+
138
+ await run.wait.aio(quiet=True)
139
+ run_details = await run.details.aio()
140
+
141
+ elapsed = str(datetime.now(timezone.utc) - start).split(".")[0]
142
+
143
+ if run_details.action_details.raw_phase == run_definition_pb2.PHASE_SUCCEEDED:
144
+ logger.warning(f"[bold green]✅ Build completed in {elapsed}![/bold green]")
145
+ else:
146
+ raise flyte.errors.ImageBuildError(f"❌ Build failed in {elapsed} at {run.url}")
147
+
148
+ outputs = await run_details.outputs()
149
+ return _get_fully_qualified_image_name(outputs)
150
+
151
+
152
+ async def _validate_configuration(image: Image) -> Tuple[str, Optional[str]]:
153
+ """Validate the configuration and prepare the spec and context files.""" # Prepare the spec file
154
+ tmp_path = Path(tempfile.gettempdir()) / str(uuid4())
155
+ os.makedirs(tmp_path, exist_ok=True)
156
+
157
+ context_path = tmp_path / "build.uc-image-builder"
158
+ context_path.mkdir(exist_ok=True)
159
+
160
+ image_idl = _get_layers_proto(image, context_path)
161
+
162
+ spec_path = tmp_path / "spec.pb"
163
+ with spec_path.open("wb") as f:
164
+ f.write(image_idl.SerializeToString())
165
+
166
+ _, spec_url = await remote.upload_file.aio(spec_path)
167
+
168
+ if any(context_path.iterdir()):
169
+ # If there are files in the context directory, upload it
170
+ tar_path = tmp_path / "context.tar"
171
+ with tarfile.open(tar_path, "w", dereference=False) as tar:
172
+ files: typing.List[str] = os.listdir(context_path)
173
+ for ws_file in files:
174
+ tar.add(
175
+ os.path.join(context_path, ws_file),
176
+ recursive=True,
177
+ arcname=ws_file,
178
+ filter=tar_strip_file_attributes,
179
+ )
180
+ context_dst = Path(f"{tar_path!s}.gz")
181
+ with gzip.GzipFile(filename=context_dst, mode="wb", mtime=0) as gzipped:
182
+ async with aiofiles.open(tar_path, "rb") as tar_file:
183
+ content = await tar_file.read()
184
+ gzipped.write(content)
185
+
186
+ context_size = tar_path.stat().st_size
187
+ if context_size > 5 * 1024 * 1024:
188
+ logger.warning(
189
+ f"[yellow]Context size is {context_size / (1024 * 1024):.2f} MB, which is larger than 5 MB. "
190
+ "Upload and build speed will be impacted.[/yellow]",
191
+ )
192
+ _, context_url = await remote.upload_file.aio(context_dst)
193
+ else:
194
+ context_url = ""
195
+
196
+ return spec_url, context_url
197
+
198
+
199
+ def _get_layers_proto(image: Image, context_path: Path) -> "image_definition_pb2.ImageSpec":
200
+ from flyteidl2.imagebuilder import definition_pb2 as image_definition_pb2
201
+
202
+ if image.dockerfile is not None:
203
+ raise flyte.errors.ImageBuildError(
204
+ "Custom Dockerfile is not supported with remote image builder.You can use local image builder instead."
205
+ )
206
+
207
+ layers = []
208
+ for layer in image._layers:
209
+ secret_mounts = None
210
+ pip_options = image_definition_pb2.PipOptions()
211
+
212
+ if isinstance(layer, PipOption):
213
+ pip_options = image_definition_pb2.PipOptions(
214
+ index_url=layer.index_url,
215
+ extra_index_urls=layer.extra_index_urls,
216
+ pre=layer.pre,
217
+ extra_args=layer.extra_args,
218
+ )
219
+
220
+ if hasattr(layer, "secret_mounts"):
221
+ sc = get_security_context(layer.secret_mounts)
222
+ secret_mounts = sc.secrets if sc else None
223
+
224
+ if isinstance(layer, AptPackages):
225
+ apt_layer = image_definition_pb2.Layer(
226
+ apt_packages=image_definition_pb2.AptPackages(
227
+ packages=layer.packages,
228
+ secret_mounts=secret_mounts,
229
+ ),
230
+ )
231
+ layers.append(apt_layer)
232
+ elif isinstance(layer, PythonWheels):
233
+ dst_path = copy_files_to_context(layer.wheel_dir, context_path)
234
+ wheel_layer = image_definition_pb2.Layer(
235
+ python_wheels=image_definition_pb2.PythonWheels(
236
+ dir=str(dst_path.relative_to(context_path)),
237
+ options=pip_options,
238
+ secret_mounts=secret_mounts,
239
+ )
240
+ )
241
+ layers.append(wheel_layer)
242
+
243
+ elif isinstance(layer, Requirements):
244
+ dst_path = copy_files_to_context(layer.file, context_path)
245
+ requirements_layer = image_definition_pb2.Layer(
246
+ requirements=image_definition_pb2.Requirements(
247
+ file=str(dst_path.relative_to(context_path)),
248
+ options=pip_options,
249
+ secret_mounts=secret_mounts,
250
+ )
251
+ )
252
+ layers.append(requirements_layer)
253
+ elif isinstance(layer, PipPackages) or isinstance(layer, UVScript):
254
+ if isinstance(layer, UVScript):
255
+ from flyte._utils import parse_uv_script_file
256
+
257
+ header = parse_uv_script_file(layer.script)
258
+ if not header.dependencies:
259
+ continue
260
+ packages: typing.Iterable[str] = header.dependencies
261
+ else:
262
+ packages = layer.packages or []
263
+ pip_layer = image_definition_pb2.Layer(
264
+ pip_packages=image_definition_pb2.PipPackages(
265
+ packages=packages,
266
+ options=pip_options,
267
+ secret_mounts=secret_mounts,
268
+ )
269
+ )
270
+ layers.append(pip_layer)
271
+ elif isinstance(layer, UVProject):
272
+ for line in layer.pyproject.read_text().splitlines():
273
+ if "tool.uv.index" in line:
274
+ raise ValueError("External sources are not supported in pyproject.toml")
275
+
276
+ if layer.project_install_mode == "dependencies_only":
277
+ # Copy pyproject itself
278
+ pyproject_dst = copy_files_to_context(layer.pyproject, context_path)
279
+ if pip_options.extra_args:
280
+ if "--no-install-project" not in pip_options.extra_args:
281
+ pip_options.extra_args += " --no-install-project"
282
+ else:
283
+ pip_options.extra_args = " --no-install-project"
284
+ if "--no-sources" not in pip_options.extra_args:
285
+ pip_options.extra_args += " --no-sources"
286
+ else:
287
+ # Copy the entire project
288
+ docker_ignore_patterns = get_and_list_dockerignore(image)
289
+ pyproject_dst = copy_files_to_context(layer.pyproject.parent, context_path, docker_ignore_patterns)
290
+
291
+ uv_layer = image_definition_pb2.Layer(
292
+ uv_project=image_definition_pb2.UVProject(
293
+ pyproject=str(pyproject_dst.relative_to(context_path)),
294
+ uvlock=str(copy_files_to_context(layer.uvlock, context_path).relative_to(context_path)),
295
+ options=pip_options,
296
+ secret_mounts=secret_mounts,
297
+ )
298
+ )
299
+ layers.append(uv_layer)
300
+ elif isinstance(layer, PoetryProject):
301
+ for line in layer.pyproject.read_text().splitlines():
302
+ if "tool.poetry.source" in line:
303
+ raise ValueError("External sources are not supported in pyproject.toml")
304
+ extra_args = layer.extra_args or ""
305
+ if layer.project_install_mode == "dependencies_only":
306
+ # Copy pyproject itself
307
+ if "--no-root" not in extra_args:
308
+ extra_args += " --no-root"
309
+ pyproject_dst = copy_files_to_context(layer.pyproject, context_path)
310
+ else:
311
+ # Copy the entire project
312
+ pyproject_dst = copy_files_to_context(layer.pyproject.parent, context_path)
313
+
314
+ poetry_layer = image_definition_pb2.Layer(
315
+ poetry_project=image_definition_pb2.PoetryProject(
316
+ pyproject=str(pyproject_dst.relative_to(context_path)),
317
+ poetry_lock=str(copy_files_to_context(layer.poetry_lock, context_path).relative_to(context_path)),
318
+ extra_args=extra_args,
319
+ secret_mounts=secret_mounts,
320
+ )
321
+ )
322
+ layers.append(poetry_layer)
323
+ elif isinstance(layer, Commands):
324
+ commands_layer = image_definition_pb2.Layer(
325
+ commands=image_definition_pb2.Commands(
326
+ cmd=list(layer.commands),
327
+ secret_mounts=secret_mounts,
328
+ )
329
+ )
330
+ layers.append(commands_layer)
331
+ elif isinstance(layer, DockerIgnore):
332
+ shutil.copy(layer.path, context_path)
333
+ elif isinstance(layer, CopyConfig):
334
+ dst_path = copy_files_to_context(layer.src, context_path)
335
+
336
+ copy_layer = image_definition_pb2.Layer(
337
+ copy_config=image_definition_pb2.CopyConfig(
338
+ src=str(dst_path.relative_to(context_path)),
339
+ dst=str(layer.dst),
340
+ )
341
+ )
342
+ layers.append(copy_layer)
343
+ elif isinstance(layer, Env):
344
+ env_layer = image_definition_pb2.Layer(
345
+ env=image_definition_pb2.Env(
346
+ env_variables=dict(layer.env_vars),
347
+ )
348
+ )
349
+ layers.append(env_layer)
350
+ elif isinstance(layer, WorkDir):
351
+ workdir_layer = image_definition_pb2.Layer(
352
+ workdir=image_definition_pb2.WorkDir(workdir=layer.workdir),
353
+ )
354
+ layers.append(workdir_layer)
355
+
356
+ return image_definition_pb2.ImageSpec(
357
+ base_image=image.base_image,
358
+ python_version=f"{image.python_version[0]}.{image.python_version[1]}",
359
+ layers=layers,
360
+ )
361
+
362
+
363
+ def _get_fully_qualified_image_name(outputs: ActionOutputs) -> str:
364
+ return outputs.pb2.literals[0].value.scalar.primitive.string_value
365
+
366
+
367
+ def _get_build_secrets_from_image(image: Image) -> Optional[typing.List[Secret]]:
368
+ secrets = []
369
+ DEFAULT_SECRET_DIR = Path("/etc/flyte/secrets")
370
+ for layer in image._layers:
371
+ if isinstance(layer, (PipOption, Commands, AptPackages)) and layer.secret_mounts is not None:
372
+ for secret_mount in layer.secret_mounts:
373
+ # Mount all the image secrets to a default directory that will be passed to the BuildKit server.
374
+ if isinstance(secret_mount, Secret):
375
+ secrets.append(Secret(key=secret_mount.key, group=secret_mount.group, mount=DEFAULT_SECRET_DIR))
376
+ elif isinstance(secret_mount, str):
377
+ secrets.append(Secret(key=secret_mount, mount=DEFAULT_SECRET_DIR))
378
+ else:
379
+ raise ValueError(f"Unsupported secret_mount type: {type(secret_mount)}")
380
+
381
+ image_registry_secret = image._image_registry_secret
382
+ if image_registry_secret:
383
+ secrets.append(
384
+ Secret(key=image_registry_secret.key, group=image_registry_secret.group, mount=DEFAULT_SECRET_DIR)
385
+ )
386
+ return secrets