flyte 2.0.0b20__py3-none-any.whl → 2.0.0b22__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.
- flyte/_bin/runtime.py +3 -3
- flyte/_code_bundle/_ignore.py +11 -3
- flyte/_code_bundle/_packaging.py +9 -5
- flyte/_code_bundle/_utils.py +2 -2
- flyte/_deploy.py +16 -8
- flyte/_image.py +5 -1
- flyte/_initialize.py +21 -8
- flyte/_interface.py +37 -2
- flyte/_internal/imagebuild/docker_builder.py +61 -8
- flyte/_internal/imagebuild/image_builder.py +8 -7
- flyte/_internal/imagebuild/remote_builder.py +9 -26
- flyte/_internal/runtime/task_serde.py +16 -6
- flyte/_keyring/__init__.py +0 -0
- flyte/_keyring/file.py +85 -0
- flyte/_logging.py +19 -8
- flyte/_task_environment.py +1 -1
- flyte/_utils/coro_management.py +2 -1
- flyte/_version.py +3 -3
- flyte/cli/_common.py +2 -2
- flyte/cli/_deploy.py +11 -1
- flyte/cli/_run.py +13 -3
- flyte/config/_config.py +6 -4
- flyte/config/_reader.py +19 -4
- flyte/git/_config.py +2 -0
- flyte/io/_dataframe/dataframe.py +3 -2
- flyte/io/_dir.py +72 -72
- flyte/models.py +6 -2
- flyte/remote/_action.py +9 -8
- flyte/remote/_client/auth/_authenticators/device_code.py +3 -4
- flyte/remote/_data.py +2 -3
- flyte/remote/_run.py +17 -1
- flyte/storage/_config.py +5 -1
- flyte/types/_pickle.py +18 -4
- flyte/types/_type_engine.py +13 -0
- {flyte-2.0.0b20.data → flyte-2.0.0b22.data}/scripts/runtime.py +3 -3
- {flyte-2.0.0b20.dist-info → flyte-2.0.0b22.dist-info}/METADATA +1 -1
- {flyte-2.0.0b20.dist-info → flyte-2.0.0b22.dist-info}/RECORD +42 -40
- {flyte-2.0.0b20.dist-info → flyte-2.0.0b22.dist-info}/entry_points.txt +3 -0
- {flyte-2.0.0b20.data → flyte-2.0.0b22.data}/scripts/debug.py +0 -0
- {flyte-2.0.0b20.dist-info → flyte-2.0.0b22.dist-info}/WHEEL +0 -0
- {flyte-2.0.0b20.dist-info → flyte-2.0.0b22.dist-info}/licenses/LICENSE +0 -0
- {flyte-2.0.0b20.dist-info → flyte-2.0.0b22.dist-info}/top_level.txt +0 -0
flyte/_bin/runtime.py
CHANGED
|
@@ -21,8 +21,8 @@ import click
|
|
|
21
21
|
|
|
22
22
|
ACTION_NAME = "ACTION_NAME"
|
|
23
23
|
RUN_NAME = "RUN_NAME"
|
|
24
|
-
PROJECT_NAME = "
|
|
25
|
-
DOMAIN_NAME = "
|
|
24
|
+
PROJECT_NAME = "FLYTE_INTERNAL_EXECUTION_PROJECT"
|
|
25
|
+
DOMAIN_NAME = "FLYTE_INTERNAL_EXECUTION_DOMAIN"
|
|
26
26
|
ORG_NAME = "_U_ORG_NAME"
|
|
27
27
|
ENDPOINT_OVERRIDE = "_U_EP_OVERRIDE"
|
|
28
28
|
RUN_OUTPUT_BASE_DIR = "_U_RUN_BASE"
|
|
@@ -137,7 +137,7 @@ def main(
|
|
|
137
137
|
logger.debug(f"Using controller endpoint: {ep} with kwargs: {controller_kwargs}")
|
|
138
138
|
|
|
139
139
|
bundle = CodeBundle(tgz=tgz, pkl=pkl, destination=dest, computed_version=version)
|
|
140
|
-
init(org=org, project=project, domain=domain, **controller_kwargs)
|
|
140
|
+
init(org=org, project=project, domain=domain, image_builder="remote", **controller_kwargs)
|
|
141
141
|
# Controller is created with the same kwargs as init, so that it can be used to run tasks
|
|
142
142
|
controller = create_controller(ct="remote", **controller_kwargs)
|
|
143
143
|
|
flyte/_code_bundle/_ignore.py
CHANGED
|
@@ -83,8 +83,15 @@ class StandardIgnore(Ignore):
|
|
|
83
83
|
self.patterns = patterns if patterns else STANDARD_IGNORE_PATTERNS
|
|
84
84
|
|
|
85
85
|
def _is_ignored(self, path: pathlib.Path) -> bool:
|
|
86
|
+
# Convert to relative path for pattern matching
|
|
87
|
+
try:
|
|
88
|
+
rel_path = path.relative_to(self.root)
|
|
89
|
+
except ValueError:
|
|
90
|
+
# If path is not under root, don't ignore it
|
|
91
|
+
return False
|
|
92
|
+
|
|
86
93
|
for pattern in self.patterns:
|
|
87
|
-
if fnmatch(str(
|
|
94
|
+
if fnmatch(str(rel_path), pattern):
|
|
88
95
|
return True
|
|
89
96
|
return False
|
|
90
97
|
|
|
@@ -105,9 +112,10 @@ class IgnoreGroup(Ignore):
|
|
|
105
112
|
|
|
106
113
|
def list_ignored(self) -> List[str]:
|
|
107
114
|
ignored = []
|
|
108
|
-
for dir, _, files in self.root
|
|
115
|
+
for dir, _, files in os.walk(self.root):
|
|
116
|
+
dir_path = Path(dir)
|
|
109
117
|
for file in files:
|
|
110
|
-
abs_path =
|
|
118
|
+
abs_path = dir_path / file
|
|
111
119
|
if self.is_ignored(abs_path):
|
|
112
120
|
ignored.append(str(abs_path.relative_to(self.root)))
|
|
113
121
|
return ignored
|
flyte/_code_bundle/_packaging.py
CHANGED
|
@@ -14,10 +14,9 @@ import typing
|
|
|
14
14
|
from typing import List, Optional, Tuple, Union
|
|
15
15
|
|
|
16
16
|
import click
|
|
17
|
-
from rich import print as rich_print
|
|
18
17
|
from rich.tree import Tree
|
|
19
18
|
|
|
20
|
-
from flyte._logging import logger
|
|
19
|
+
from flyte._logging import _get_console, logger
|
|
21
20
|
|
|
22
21
|
from ._ignore import Ignore, IgnoreGroup
|
|
23
22
|
from ._utils import CopyFiles, _filehash_update, _pathhash_update, ls_files, tar_strip_file_attributes
|
|
@@ -27,10 +26,10 @@ FAST_FILEENDING = ".tar.gz"
|
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
def print_ls_tree(source: os.PathLike, ls: typing.List[str]):
|
|
30
|
-
|
|
29
|
+
logger.info("Files to be copied for fast registration...")
|
|
31
30
|
|
|
32
31
|
tree_root = Tree(
|
|
33
|
-
f":open_file_folder:
|
|
32
|
+
f"File structure:\n:open_file_folder: {source}",
|
|
34
33
|
guide_style="bold bright_blue",
|
|
35
34
|
)
|
|
36
35
|
trees = {pathlib.Path(source): tree_root}
|
|
@@ -49,7 +48,12 @@ def print_ls_tree(source: os.PathLike, ls: typing.List[str]):
|
|
|
49
48
|
else:
|
|
50
49
|
current = trees[current_path]
|
|
51
50
|
trees[fpp.parent].add(f"{fpp.name}", guide_style="bold bright_blue")
|
|
52
|
-
|
|
51
|
+
|
|
52
|
+
console = _get_console()
|
|
53
|
+
with console.capture() as capture:
|
|
54
|
+
console.print(tree_root, overflow="ignore", no_wrap=True, crop=False)
|
|
55
|
+
logger.info(f"Root directory: [link=file://{source}]{source}[/link]")
|
|
56
|
+
logger.info(capture.get(), extra={"console": console})
|
|
53
57
|
|
|
54
58
|
|
|
55
59
|
def _compress_tarball(source: pathlib.Path, output: pathlib.Path) -> None:
|
flyte/_code_bundle/_utils.py
CHANGED
|
@@ -156,7 +156,7 @@ def list_all_files(source_path: pathlib.Path, deref_symlinks, ignore_group: Opti
|
|
|
156
156
|
|
|
157
157
|
# This is needed to prevent infinite recursion when walking with followlinks
|
|
158
158
|
visited_inodes = set()
|
|
159
|
-
for root, dirnames, files in
|
|
159
|
+
for root, dirnames, files in os.walk(source_path, topdown=True, followlinks=deref_symlinks):
|
|
160
160
|
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
|
|
161
161
|
if deref_symlinks:
|
|
162
162
|
inode = os.stat(root).st_ino
|
|
@@ -167,7 +167,7 @@ def list_all_files(source_path: pathlib.Path, deref_symlinks, ignore_group: Opti
|
|
|
167
167
|
ff = []
|
|
168
168
|
files.sort()
|
|
169
169
|
for fname in files:
|
|
170
|
-
abspath = (root / fname).absolute()
|
|
170
|
+
abspath = (pathlib.Path(root) / fname).absolute()
|
|
171
171
|
# Only consider files that exist (e.g. disregard symlinks that point to non-existent files)
|
|
172
172
|
if not os.path.exists(abspath):
|
|
173
173
|
logger.info(f"Skipping non-existent file {abspath}")
|
flyte/_deploy.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
import sys
|
|
4
6
|
import typing
|
|
5
7
|
from dataclasses import dataclass
|
|
6
8
|
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
|
7
9
|
|
|
10
|
+
import cloudpickle
|
|
8
11
|
import rich.repr
|
|
9
12
|
|
|
10
13
|
import flyte.errors
|
|
@@ -160,9 +163,11 @@ async def _build_images(deployment: DeploymentPlan) -> ImageCache:
|
|
|
160
163
|
logger.warning(f"Built Image for environment {env_name}, image: {image_uri}")
|
|
161
164
|
env = deployment.envs[env_name]
|
|
162
165
|
if isinstance(env.image, Image):
|
|
163
|
-
|
|
166
|
+
py_version = "{}.{}".format(*env.image.python_version)
|
|
167
|
+
image_identifier_map[env.image.identifier] = {py_version: image_uri}
|
|
164
168
|
elif env.image == "auto":
|
|
165
|
-
|
|
169
|
+
py_version = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
|
|
170
|
+
image_identifier_map["auto"] = {py_version: image_uri}
|
|
166
171
|
|
|
167
172
|
return ImageCache(image_lookup=image_identifier_map)
|
|
168
173
|
|
|
@@ -175,15 +180,18 @@ async def apply(deployment_plan: DeploymentPlan, copy_style: CopyFiles, dryrun:
|
|
|
175
180
|
|
|
176
181
|
image_cache = await _build_images(deployment_plan)
|
|
177
182
|
|
|
178
|
-
|
|
179
|
-
if copy_style == "none" and not version:
|
|
183
|
+
if copy_style == "none" and not deployment_plan.version:
|
|
180
184
|
raise flyte.errors.DeploymentError("Version must be set when copy_style is none")
|
|
181
185
|
else:
|
|
182
186
|
code_bundle = await build_code_bundle(from_dir=cfg.root_dir, dryrun=dryrun, copy_style=copy_style)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
+
if deployment_plan.version:
|
|
188
|
+
version = deployment_plan.version
|
|
189
|
+
else:
|
|
190
|
+
h = hashlib.md5()
|
|
191
|
+
h.update(cloudpickle.dumps(deployment_plan.envs))
|
|
192
|
+
h.update(code_bundle.computed_version.encode("utf-8"))
|
|
193
|
+
h.update(cloudpickle.dumps(image_cache))
|
|
194
|
+
version = h.hexdigest()
|
|
187
195
|
|
|
188
196
|
sc = SerializationContext(
|
|
189
197
|
project=cfg.project,
|
flyte/_image.py
CHANGED
|
@@ -203,6 +203,7 @@ class UVProject(PipOption, Layer):
|
|
|
203
203
|
|
|
204
204
|
super().update_hash(hasher)
|
|
205
205
|
filehash_update(self.uvlock, hasher)
|
|
206
|
+
filehash_update(self.pyproject, hasher)
|
|
206
207
|
|
|
207
208
|
|
|
208
209
|
@rich.repr.auto
|
|
@@ -435,7 +436,10 @@ class Image:
|
|
|
435
436
|
# Only get the non-None values in the Image to ensure the hash is consistent
|
|
436
437
|
# across different SDK versions.
|
|
437
438
|
# Layers can specify a _compute_identifier optionally, but the default will just stringify
|
|
438
|
-
image_dict = asdict(
|
|
439
|
+
image_dict = asdict(
|
|
440
|
+
self,
|
|
441
|
+
dict_factory=lambda x: {k: v for (k, v) in x if v is not None and k not in ("_layers", "python_version")},
|
|
442
|
+
)
|
|
439
443
|
layers_str_repr = "".join([layer.identifier() for layer in self._layers])
|
|
440
444
|
image_dict["layers"] = layers_str_repr
|
|
441
445
|
spec_bytes = image_dict.__str__().encode("utf-8")
|
flyte/_initialize.py
CHANGED
|
@@ -33,6 +33,7 @@ class CommonInit:
|
|
|
33
33
|
project: str | None = None
|
|
34
34
|
domain: str | None = None
|
|
35
35
|
batch_size: int = 1000
|
|
36
|
+
source_config_path: Optional[Path] = None # Only used for documentation
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
@dataclass(init=True, kw_only=True, repr=True, eq=True, frozen=True)
|
|
@@ -110,6 +111,12 @@ async def _initialize_client(
|
|
|
110
111
|
)
|
|
111
112
|
|
|
112
113
|
|
|
114
|
+
def _initialize_logger(log_level: int | None = None):
|
|
115
|
+
initialize_logger(enable_rich=True)
|
|
116
|
+
if log_level:
|
|
117
|
+
initialize_logger(log_level=log_level, enable_rich=True)
|
|
118
|
+
|
|
119
|
+
|
|
113
120
|
@syncify
|
|
114
121
|
async def init(
|
|
115
122
|
org: str | None = None,
|
|
@@ -134,6 +141,7 @@ async def init(
|
|
|
134
141
|
storage: Storage | None = None,
|
|
135
142
|
batch_size: int = 1000,
|
|
136
143
|
image_builder: ImageBuildEngine.ImageBuilderType = "local",
|
|
144
|
+
source_config_path: Optional[Path] = None,
|
|
137
145
|
) -> None:
|
|
138
146
|
"""
|
|
139
147
|
Initialize the Flyte system with the given configuration. This method should be called before any other Flyte
|
|
@@ -169,17 +177,12 @@ async def init(
|
|
|
169
177
|
:param batch_size: Optional batch size for operations that use listings, defaults to 1000, so limit larger than
|
|
170
178
|
batch_size will be split into multiple requests.
|
|
171
179
|
:param image_builder: Optional image builder configuration, if not provided, the default image builder will be used.
|
|
172
|
-
|
|
180
|
+
:param source_config_path: Optional path to the source configuration file (This is only used for documentation)
|
|
173
181
|
:return: None
|
|
174
182
|
"""
|
|
175
|
-
from flyte._tools import ipython_check
|
|
176
183
|
from flyte._utils import get_cwd_editable_install, org_from_endpoint, sanitize_endpoint
|
|
177
184
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
initialize_logger(enable_rich=interactive_mode)
|
|
181
|
-
if log_level:
|
|
182
|
-
initialize_logger(log_level=log_level, enable_rich=interactive_mode)
|
|
185
|
+
_initialize_logger(log_level=log_level)
|
|
183
186
|
|
|
184
187
|
global _init_config # noqa: PLW0603
|
|
185
188
|
|
|
@@ -223,6 +226,7 @@ async def init(
|
|
|
223
226
|
org=org or org_from_endpoint(endpoint),
|
|
224
227
|
batch_size=batch_size,
|
|
225
228
|
image_builder=image_builder,
|
|
229
|
+
source_config_path=source_config_path,
|
|
226
230
|
)
|
|
227
231
|
|
|
228
232
|
|
|
@@ -231,6 +235,7 @@ async def init_from_config(
|
|
|
231
235
|
path_or_config: str | Path | Config | None = None,
|
|
232
236
|
root_dir: Path | None = None,
|
|
233
237
|
log_level: int | None = None,
|
|
238
|
+
storage: Storage | None = None,
|
|
234
239
|
) -> None:
|
|
235
240
|
"""
|
|
236
241
|
Initialize the Flyte system using a configuration file or Config object. This method should be called before any
|
|
@@ -243,11 +248,15 @@ async def init_from_config(
|
|
|
243
248
|
if not available, the current working directory.
|
|
244
249
|
:param log_level: Optional logging level for the framework logger,
|
|
245
250
|
default is set using the default initialization policies
|
|
251
|
+
:param storage: Optional blob store (S3, GCS, Azure) configuration if needed to access (i.e. using Minio)
|
|
246
252
|
:return: None
|
|
247
253
|
"""
|
|
254
|
+
from rich.highlighter import ReprHighlighter
|
|
255
|
+
|
|
248
256
|
import flyte.config as config
|
|
249
257
|
|
|
250
258
|
cfg: config.Config
|
|
259
|
+
cfg_path: Optional[Path] = None
|
|
251
260
|
if path_or_config is None:
|
|
252
261
|
# If no path is provided, use the default config file
|
|
253
262
|
cfg = config.auto()
|
|
@@ -266,7 +275,9 @@ async def init_from_config(
|
|
|
266
275
|
else:
|
|
267
276
|
cfg = path_or_config
|
|
268
277
|
|
|
269
|
-
|
|
278
|
+
_initialize_logger(log_level=log_level)
|
|
279
|
+
|
|
280
|
+
logger.info(f"Flyte config initialized as {cfg}", extra={"highlighter": ReprHighlighter()})
|
|
270
281
|
await init.aio(
|
|
271
282
|
org=cfg.task.org,
|
|
272
283
|
project=cfg.task.project,
|
|
@@ -283,6 +294,8 @@ async def init_from_config(
|
|
|
283
294
|
root_dir=root_dir,
|
|
284
295
|
log_level=log_level,
|
|
285
296
|
image_builder=cfg.image.builder,
|
|
297
|
+
storage=storage,
|
|
298
|
+
source_config_path=cfg_path,
|
|
286
299
|
)
|
|
287
300
|
|
|
288
301
|
|
flyte/_interface.py
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
-
|
|
4
|
+
import typing
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Dict, Generator, Literal, Tuple, Type, TypeVar, Union, cast, get_args, get_origin, get_type_hints
|
|
7
|
+
|
|
8
|
+
from flyte._logging import logger
|
|
9
|
+
|
|
10
|
+
LITERAL_ENUM = "LiteralEnum"
|
|
5
11
|
|
|
6
12
|
|
|
7
13
|
def default_output_name(index: int = 0) -> str:
|
|
@@ -69,7 +75,15 @@ def extract_return_annotation(return_annotation: Union[Type, Tuple, None]) -> Di
|
|
|
69
75
|
if len(return_annotation.__args__) == 1: # type: ignore
|
|
70
76
|
raise TypeError("Tuples should be used to indicate multiple return values, found only one return variable.")
|
|
71
77
|
ra = get_args(return_annotation)
|
|
72
|
-
|
|
78
|
+
annotations = {}
|
|
79
|
+
for i, r in enumerate(ra):
|
|
80
|
+
if r is Ellipsis:
|
|
81
|
+
raise TypeError("Variable length tuples are not supported as return types.")
|
|
82
|
+
if get_origin(r) is Literal:
|
|
83
|
+
annotations[default_output_name(i)] = literal_to_enum(cast(Type, r))
|
|
84
|
+
else:
|
|
85
|
+
annotations[default_output_name(i)] = r
|
|
86
|
+
return annotations
|
|
73
87
|
|
|
74
88
|
elif isinstance(return_annotation, tuple):
|
|
75
89
|
if len(return_annotation) == 1:
|
|
@@ -79,4 +93,25 @@ def extract_return_annotation(return_annotation: Union[Type, Tuple, None]) -> Di
|
|
|
79
93
|
else:
|
|
80
94
|
# Handle all other single return types
|
|
81
95
|
# Task returns unnamed native tuple
|
|
96
|
+
if get_origin(return_annotation) is Literal:
|
|
97
|
+
return {default_output_name(): literal_to_enum(cast(Type, return_annotation))}
|
|
82
98
|
return {default_output_name(): cast(Type, return_annotation)}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def literal_to_enum(literal_type: Type) -> Type[Enum | typing.Any]:
|
|
102
|
+
"""Convert a Literal[...] into Union[str, Enum]."""
|
|
103
|
+
|
|
104
|
+
if get_origin(literal_type) is not Literal:
|
|
105
|
+
raise TypeError(f"{literal_type} is not a Literal")
|
|
106
|
+
|
|
107
|
+
values = get_args(literal_type)
|
|
108
|
+
if not all(isinstance(v, str) for v in values):
|
|
109
|
+
logger.warning(f"Literal type {literal_type} contains non-string values, using Any instead of Enum")
|
|
110
|
+
return typing.Any
|
|
111
|
+
# Deduplicate & keep order
|
|
112
|
+
enum_dict = {str(v).upper(): v for v in values}
|
|
113
|
+
|
|
114
|
+
# Dynamically create an Enum
|
|
115
|
+
literal_enum = Enum(LITERAL_ENUM, enum_dict) # type: ignore
|
|
116
|
+
|
|
117
|
+
return literal_enum # type: ignore
|
|
@@ -6,7 +6,7 @@ import tempfile
|
|
|
6
6
|
import typing
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from string import Template
|
|
9
|
-
from typing import ClassVar, Optional, Protocol, cast
|
|
9
|
+
from typing import ClassVar, List, Optional, Protocol, cast
|
|
10
10
|
|
|
11
11
|
import aiofiles
|
|
12
12
|
import click
|
|
@@ -245,6 +245,9 @@ class UVProjectHandler:
|
|
|
245
245
|
else:
|
|
246
246
|
# Copy the entire project.
|
|
247
247
|
pyproject_dst = copy_files_to_context(layer.pyproject.parent, context_path)
|
|
248
|
+
if layer.uvlock:
|
|
249
|
+
# Sometimes the uv.lock file is in a different folder, if it's specified, let's copy it there explicitly
|
|
250
|
+
shutil.copy(layer.uvlock, pyproject_dst)
|
|
248
251
|
delta = UV_LOCK_INSTALL_TEMPLATE.substitute(
|
|
249
252
|
PYPROJECT_PATH=pyproject_dst.relative_to(context_path),
|
|
250
253
|
PIP_INSTALL_ARGS=" ".join(layer.get_pip_install_args()),
|
|
@@ -263,7 +266,37 @@ class DockerIgnoreHandler:
|
|
|
263
266
|
|
|
264
267
|
class CopyConfigHandler:
|
|
265
268
|
@staticmethod
|
|
266
|
-
|
|
269
|
+
def list_dockerignore(root_path: Optional[Path], docker_ignore_file_path: Optional[Path]) -> List[str]:
|
|
270
|
+
patterns: List[str] = []
|
|
271
|
+
dockerignore_path: Optional[Path] = None
|
|
272
|
+
if root_path:
|
|
273
|
+
dockerignore_path = root_path / ".dockerignore"
|
|
274
|
+
# DockerIgnore layer should be first priority
|
|
275
|
+
if docker_ignore_file_path:
|
|
276
|
+
dockerignore_path = docker_ignore_file_path
|
|
277
|
+
|
|
278
|
+
# Return empty list if no .dockerignore file found
|
|
279
|
+
if not dockerignore_path or not dockerignore_path.exists() or not dockerignore_path.is_file():
|
|
280
|
+
logger.info(f".dockerignore file not found at path: {dockerignore_path}")
|
|
281
|
+
return patterns
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
with open(dockerignore_path, "r", encoding="utf-8") as f:
|
|
285
|
+
for line in f:
|
|
286
|
+
stripped_line = line.strip()
|
|
287
|
+
# Skip empty lines, whitespace-only lines, and comments
|
|
288
|
+
if not stripped_line or stripped_line.startswith("#"):
|
|
289
|
+
continue
|
|
290
|
+
patterns.append(stripped_line)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.error(f"Failed to read .dockerignore file at {dockerignore_path}: {e}")
|
|
293
|
+
return []
|
|
294
|
+
return patterns
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
async def handle(
|
|
298
|
+
layer: CopyConfig, context_path: Path, dockerfile: str, docker_ignore_file_path: Optional[Path]
|
|
299
|
+
) -> str:
|
|
267
300
|
# Copy the source config file or directory to the context path
|
|
268
301
|
if layer.src.is_absolute() or ".." in str(layer.src):
|
|
269
302
|
dst_path = context_path / str(layer.src.absolute()).replace("/", "./_flyte_abs_context/", 1)
|
|
@@ -272,18 +305,26 @@ class CopyConfigHandler:
|
|
|
272
305
|
|
|
273
306
|
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
274
307
|
abs_path = layer.src.absolute()
|
|
308
|
+
|
|
275
309
|
if layer.src.is_file():
|
|
276
310
|
# Copy the file
|
|
277
311
|
shutil.copy(abs_path, dst_path)
|
|
278
312
|
elif layer.src.is_dir():
|
|
279
313
|
# Copy the entire directory
|
|
280
|
-
|
|
314
|
+
from flyte._initialize import _get_init_config
|
|
315
|
+
|
|
316
|
+
init_config = _get_init_config()
|
|
317
|
+
root_path = init_config.root_dir if init_config else None
|
|
318
|
+
docker_ignore_patterns = CopyConfigHandler.list_dockerignore(root_path, docker_ignore_file_path)
|
|
319
|
+
shutil.copytree(
|
|
320
|
+
abs_path, dst_path, dirs_exist_ok=True, ignore=shutil.ignore_patterns(*docker_ignore_patterns)
|
|
321
|
+
)
|
|
281
322
|
else:
|
|
282
|
-
|
|
323
|
+
logger.error(f"Source path not exists: {layer.src}")
|
|
324
|
+
return dockerfile
|
|
283
325
|
|
|
284
326
|
# Add a copy command to the dockerfile
|
|
285
327
|
dockerfile += f"\nCOPY {dst_path.relative_to(context_path)} {layer.dst}\n"
|
|
286
|
-
|
|
287
328
|
return dockerfile
|
|
288
329
|
|
|
289
330
|
|
|
@@ -349,7 +390,9 @@ def _get_secret_mounts_layer(secrets: typing.Tuple[str | Secret, ...] | None) ->
|
|
|
349
390
|
return secret_mounts_layer
|
|
350
391
|
|
|
351
392
|
|
|
352
|
-
async def _process_layer(
|
|
393
|
+
async def _process_layer(
|
|
394
|
+
layer: Layer, context_path: Path, dockerfile: str, docker_ignore_file_path: Optional[Path]
|
|
395
|
+
) -> str:
|
|
353
396
|
match layer:
|
|
354
397
|
case PythonWheels():
|
|
355
398
|
# Handle Python wheels
|
|
@@ -385,7 +428,7 @@ async def _process_layer(layer: Layer, context_path: Path, dockerfile: str) -> s
|
|
|
385
428
|
|
|
386
429
|
case CopyConfig():
|
|
387
430
|
# Handle local files and folders
|
|
388
|
-
dockerfile = await CopyConfigHandler.handle(layer, context_path, dockerfile)
|
|
431
|
+
dockerfile = await CopyConfigHandler.handle(layer, context_path, dockerfile, docker_ignore_file_path)
|
|
389
432
|
|
|
390
433
|
case Commands():
|
|
391
434
|
# Handle commands
|
|
@@ -512,6 +555,15 @@ class DockerImageBuilder(ImageBuilder):
|
|
|
512
555
|
else:
|
|
513
556
|
logger.info("Buildx builder already exists.")
|
|
514
557
|
|
|
558
|
+
def get_docker_ignore(self, image: Image) -> Optional[Path]:
|
|
559
|
+
"""Get the .dockerignore file path from the image layers."""
|
|
560
|
+
# Look for DockerIgnore layer in the image layers
|
|
561
|
+
for layer in image._layers:
|
|
562
|
+
if isinstance(layer, DockerIgnore) and layer.path.strip():
|
|
563
|
+
return Path(layer.path)
|
|
564
|
+
|
|
565
|
+
return None
|
|
566
|
+
|
|
515
567
|
async def _build_image(self, image: Image, *, push: bool = True, dry_run: bool = False) -> str:
|
|
516
568
|
"""
|
|
517
569
|
if default image (only base image and locked), raise an error, don't have a dockerfile
|
|
@@ -541,8 +593,9 @@ class DockerImageBuilder(ImageBuilder):
|
|
|
541
593
|
PYTHON_VERSION=f"{image.python_version[0]}.{image.python_version[1]}",
|
|
542
594
|
)
|
|
543
595
|
|
|
596
|
+
docker_ignore_file_path = self.get_docker_ignore(image)
|
|
544
597
|
for layer in image._layers:
|
|
545
|
-
dockerfile = await _process_layer(layer, tmp_path, dockerfile)
|
|
598
|
+
dockerfile = await _process_layer(layer, tmp_path, dockerfile, docker_ignore_file_path)
|
|
546
599
|
|
|
547
600
|
dockerfile += DOCKER_FILE_BASE_FOOTER.substitute(F_IMG_ID=image.uri)
|
|
548
601
|
|
|
@@ -235,7 +235,7 @@ class ImageBuildEngine:
|
|
|
235
235
|
|
|
236
236
|
|
|
237
237
|
class ImageCache(BaseModel):
|
|
238
|
-
image_lookup: Dict[str, str]
|
|
238
|
+
image_lookup: Dict[str, Dict[str, str]]
|
|
239
239
|
serialized_form: str | None = None
|
|
240
240
|
|
|
241
241
|
@property
|
|
@@ -273,10 +273,11 @@ class ImageCache(BaseModel):
|
|
|
273
273
|
"""
|
|
274
274
|
tuples = []
|
|
275
275
|
for k, v in self.image_lookup.items():
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
276
|
+
for py_version, image_uri in v.items():
|
|
277
|
+
tuples.append(
|
|
278
|
+
[
|
|
279
|
+
("Name", f"{k} (py{py_version})"),
|
|
280
|
+
("image", image_uri),
|
|
281
|
+
]
|
|
282
|
+
)
|
|
282
283
|
return tuples
|
|
@@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Optional, Tuple, cast
|
|
|
10
10
|
from uuid import uuid4
|
|
11
11
|
|
|
12
12
|
import aiofiles
|
|
13
|
-
import click
|
|
14
13
|
|
|
15
14
|
import flyte
|
|
16
15
|
import flyte.errors
|
|
@@ -42,7 +41,6 @@ if TYPE_CHECKING:
|
|
|
42
41
|
from flyte._protos.imagebuilder import definition_pb2 as image_definition_pb2
|
|
43
42
|
|
|
44
43
|
IMAGE_TASK_NAME = "build-image"
|
|
45
|
-
OPTIMIZE_TASK_NAME = "optimize-task"
|
|
46
44
|
IMAGE_TASK_PROJECT = "system"
|
|
47
45
|
IMAGE_TASK_DOMAIN = "production"
|
|
48
46
|
|
|
@@ -85,10 +83,10 @@ class RemoteImageChecker(ImageChecker):
|
|
|
85
83
|
raise ValueError("remote client should not be None")
|
|
86
84
|
cls._images_client = image_service_pb2_grpc.ImageServiceStub(cfg.client._channel)
|
|
87
85
|
resp = await cls._images_client.GetImage(req)
|
|
88
|
-
logger.warning(
|
|
86
|
+
logger.warning(f"[blue]Image {resp.image.fqin} found. Skip building.[/blue]")
|
|
89
87
|
return resp.image.fqin
|
|
90
88
|
except Exception:
|
|
91
|
-
logger.warning(
|
|
89
|
+
logger.warning(f"[blue]Image {image_name} was not found or has expired.[/blue]", extra={"highlight": False})
|
|
92
90
|
return None
|
|
93
91
|
|
|
94
92
|
|
|
@@ -110,37 +108,25 @@ class RemoteImageBuilder(ImageBuilder):
|
|
|
110
108
|
domain=IMAGE_TASK_DOMAIN,
|
|
111
109
|
auto_version="latest",
|
|
112
110
|
).override.aio(secrets=_get_build_secrets_from_image(image))
|
|
111
|
+
|
|
112
|
+
logger.warning("[bold blue]🐳 Submitting a new build...[/bold blue]")
|
|
113
113
|
run = cast(
|
|
114
114
|
Run,
|
|
115
115
|
await flyte.with_runcontext(project=IMAGE_TASK_PROJECT, domain=IMAGE_TASK_DOMAIN).run.aio(
|
|
116
116
|
entity, spec=spec, context=context, target_image=image_name
|
|
117
117
|
),
|
|
118
118
|
)
|
|
119
|
-
logger.warning(
|
|
119
|
+
logger.warning(f"⏳ Waiting for build to finish at: [bold cyan link={run.url}]{run.url}[/bold cyan link]")
|
|
120
120
|
|
|
121
|
-
logger.warning(click.style("⏳ Waiting for build to finish at: " + click.style(run.url, fg="cyan"), bold=True))
|
|
122
121
|
await run.wait.aio(quiet=True)
|
|
123
122
|
run_details = await run.details.aio()
|
|
124
123
|
|
|
125
124
|
elapsed = str(datetime.now(timezone.utc) - start).split(".")[0]
|
|
126
125
|
|
|
127
126
|
if run_details.action_details.raw_phase == run_definition_pb2.PHASE_SUCCEEDED:
|
|
128
|
-
logger.warning(
|
|
129
|
-
try:
|
|
130
|
-
entity = remote.Task.get(
|
|
131
|
-
name=OPTIMIZE_TASK_NAME,
|
|
132
|
-
project=IMAGE_TASK_PROJECT,
|
|
133
|
-
domain=IMAGE_TASK_DOMAIN,
|
|
134
|
-
auto_version="latest",
|
|
135
|
-
)
|
|
136
|
-
await flyte.with_runcontext(project=IMAGE_TASK_PROJECT, domain=IMAGE_TASK_DOMAIN).run.aio(
|
|
137
|
-
entity, target_image=image_name
|
|
138
|
-
)
|
|
139
|
-
except Exception as e:
|
|
140
|
-
# Ignore the error if optimize is not enabled in the backend.
|
|
141
|
-
logger.warning(f"Failed to run optimize task with error: {e}")
|
|
127
|
+
logger.warning(f"[bold green]✅ Build completed in {elapsed}![/bold green]")
|
|
142
128
|
else:
|
|
143
|
-
raise flyte.errors.ImageBuildError(f"❌ Build failed in {elapsed} at {
|
|
129
|
+
raise flyte.errors.ImageBuildError(f"❌ Build failed in {elapsed} at [cyan]{run.url}[/cyan]")
|
|
144
130
|
|
|
145
131
|
outputs = await run_details.outputs()
|
|
146
132
|
return _get_fully_qualified_image_name(outputs)
|
|
@@ -183,11 +169,8 @@ async def _validate_configuration(image: Image) -> Tuple[str, Optional[str]]:
|
|
|
183
169
|
context_size = tar_path.stat().st_size
|
|
184
170
|
if context_size > 5 * 1024 * 1024:
|
|
185
171
|
logger.warning(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"Upload and build speed will be impacted.",
|
|
189
|
-
fg="yellow",
|
|
190
|
-
)
|
|
172
|
+
f"[yellow]Context size is {context_size / (1024 * 1024):.2f} MB, which is larger than 5 MB. "
|
|
173
|
+
"Upload and build speed will be impacted.[/yellow]",
|
|
191
174
|
)
|
|
192
175
|
_, context_url = await remote.upload_file.aio(context_dst)
|
|
193
176
|
else:
|
|
@@ -4,6 +4,7 @@ It includes a Resolver interface for loading tasks, and functions to load classe
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import copy
|
|
7
|
+
import sys
|
|
7
8
|
import typing
|
|
8
9
|
from datetime import timedelta
|
|
9
10
|
from typing import Optional, cast
|
|
@@ -217,16 +218,25 @@ def _get_urun_container(
|
|
|
217
218
|
img_uri = task_template.image.uri
|
|
218
219
|
elif serialize_context.image_cache and image_id not in serialize_context.image_cache.image_lookup:
|
|
219
220
|
img_uri = task_template.image.uri
|
|
220
|
-
from flyte._version import __version__
|
|
221
221
|
|
|
222
222
|
logger.warning(
|
|
223
|
-
f"Image {task_template.image} not found in the image cache: {serialize_context.image_cache.image_lookup}
|
|
224
|
-
f"This typically occurs when the Flyte SDK version (`{__version__}`) used in the task environment "
|
|
225
|
-
f"differs from the version used to compile or deploy it.\n"
|
|
226
|
-
f"Ensure both environments use the same Flyte SDK version to avoid inconsistencies in image resolution."
|
|
223
|
+
f"Image {task_template.image} not found in the image cache: {serialize_context.image_cache.image_lookup}."
|
|
227
224
|
)
|
|
228
225
|
else:
|
|
229
|
-
|
|
226
|
+
python_version_str = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
|
|
227
|
+
version_lookup = serialize_context.image_cache.image_lookup[image_id]
|
|
228
|
+
if python_version_str in version_lookup:
|
|
229
|
+
img_uri = version_lookup[python_version_str]
|
|
230
|
+
elif version_lookup:
|
|
231
|
+
# Fallback: try to get any available version
|
|
232
|
+
fallback_py_version, img_uri = next(iter(version_lookup.items()))
|
|
233
|
+
logger.warning(
|
|
234
|
+
f"Image {task_template.image} for python version {python_version_str} "
|
|
235
|
+
f"not found in the image cache: {serialize_context.image_cache.image_lookup}.\n"
|
|
236
|
+
f"Fall back using image {img_uri} for python version {fallback_py_version} ."
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
img_uri = task_template.image.uri
|
|
230
240
|
|
|
231
241
|
return tasks_pb2.Container(
|
|
232
242
|
image=img_uri,
|
|
File without changes
|