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,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
|