Skip to content

Commit 4248597

Browse files
mhotanthomasjpfan
andauthored
mhotan/v1.15 bp 3231 3239 (#3240)
* Adds ImageSpec.with_runtime_packages (#3231) * Adds ImageSpec.with_dev_dependencies Signed-off-by: Thomas J. Fan <[email protected]> * Fix Signed-off-by: Thomas J. Fan <[email protected]> * Add tests for noop builder Signed-off-by: Thomas J. Fan <[email protected]> * Use runtime_packages Signed-off-by: Thomas J. Fan <[email protected]> * Add docs abount how to use runtime packages Signed-off-by: Thomas J. Fan <[email protected]> * Less diffs Signed-off-by: Thomas J. Fan <[email protected]> * Fix formatting Signed-off-by: Thomas J. Fan <[email protected]> * Fix docstring Signed-off-by: Thomas J. Fan <[email protected]> * Dix docstring Signed-off-by: Thomas J. Fan <[email protected]> * Let pip default to user by itself to be more compatible Signed-off-by: Thomas J. Fan <[email protected]> --------- Signed-off-by: Thomas J. Fan <[email protected]> * Image spec builder options (#3233) * Image spec builder options Provide the ability to specify image `builder` specific options per Image Spec. Signed-off-by: Mike Hotan <[email protected]> * Add builder_options validation Signed-off-by: Mike Hotan <[email protected]> * updates Signed-off-by: Mike Hotan <[email protected]> --------- Signed-off-by: Mike Hotan <[email protected]> --------- Signed-off-by: Thomas J. Fan <[email protected]> Signed-off-by: Mike Hotan <[email protected]> Co-authored-by: Thomas J. Fan <[email protected]>
1 parent e1518f6 commit 4248597

File tree

9 files changed

+189
-50
lines changed

9 files changed

+189
-50
lines changed

flytekit/bin/entrypoint.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from flytekit.core import utils
3131
from flytekit.core.base_task import IgnoreOutputs, PythonTask
3232
from flytekit.core.checkpointer import SyncCheckpoint
33-
from flytekit.core.constants import FLYTE_FAIL_ON_ERROR
33+
from flytekit.core.constants import FLYTE_FAIL_ON_ERROR, RUNTIME_PACKAGES_ENV_NAME
3434
from flytekit.core.context_manager import (
3535
ExecutionParameters,
3636
ExecutionState,
@@ -63,6 +63,18 @@ def get_version_message():
6363
return f"Welcome to Flyte! Version: {flytekit.__version__}"
6464

6565

66+
def _run_subprocess(cmd: List[str], env: Optional[dict] = None) -> int:
67+
"""Run cmd with proper SIGTERM handling."""
68+
p = subprocess.Popen(cmd, env=env)
69+
70+
def handle_sigterm(signum, frame):
71+
logger.info(f"passing signum {signum} [frame={frame}] to subprocess")
72+
p.send_signal(signum)
73+
74+
signal.signal(signal.SIGTERM, handle_sigterm)
75+
return p.wait()
76+
77+
6678
def _compute_array_job_index():
6779
"""
6880
Computes the absolute index of the current array job. This is determined by summing the compute-environment-specific
@@ -432,6 +444,14 @@ def setup_execution(
432444

433445
compressed_serialization_settings = os.environ.get(SERIALIZED_CONTEXT_ENV_VAR, "")
434446

447+
if runtime_packages := os.getenv(RUNTIME_PACKAGES_ENV_NAME):
448+
import importlib
449+
import site
450+
451+
dev_packages_list = runtime_packages.split(" ")
452+
_run_subprocess([sys.executable, "-m", "pip", "install", *dev_packages_list])
453+
importlib.reload(site)
454+
435455
ctx = FlyteContextManager.current_context()
436456
# Create directories
437457
user_workspace_dir = ctx.file_access.get_random_local_directory()
@@ -751,14 +771,7 @@ def fast_execute_task_cmd(additional_distribution: str, dest_dir: str, task_exec
751771
env["PYTHONPATH"] += os.pathsep + dest_dir_resolved
752772
else:
753773
env["PYTHONPATH"] = dest_dir_resolved
754-
p = subprocess.Popen(cmd, env=env)
755-
756-
def handle_sigterm(signum, frame):
757-
logger.info(f"passing signum {signum} [frame={frame}] to subprocess")
758-
p.send_signal(signum)
759-
760-
signal.signal(signal.SIGTERM, handle_sigterm)
761-
returncode = p.wait()
774+
returncode = _run_subprocess(cmd, env)
762775
exit(returncode)
763776

764777

flytekit/core/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,6 @@
4444
# Shared memory mount name and path
4545
SHARED_MEMORY_MOUNT_NAME = "flyte-shared-memory"
4646
SHARED_MEMORY_MOUNT_PATH = "/dev/shm"
47+
48+
# Packages to be installed at the beginning of runtime
49+
RUNTIME_PACKAGES_ENV_NAME = "_F_RUNTIME_PACKAGES"

flytekit/core/python_auto_container.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from flytekit.configuration import ImageConfig, SerializationSettings
1313
from flytekit.constants import CopyFileDetection
1414
from flytekit.core.base_task import PythonTask, TaskMetadata, TaskResolverMixin
15+
from flytekit.core.constants import RUNTIME_PACKAGES_ENV_NAME
1516
from flytekit.core.context_manager import FlyteContextManager
1617
from flytekit.core.pod_template import PodTemplate
1718
from flytekit.core.resources import Resources, ResourceSpec, construct_extended_resources
@@ -231,6 +232,12 @@ def _get_container(self, settings: SerializationSettings) -> _task_model.Contain
231232
for elem in (settings.env, self.environment):
232233
if elem:
233234
env.update(elem)
235+
236+
# Add runtime dependencies into environment
237+
if isinstance(self.container_image, ImageSpec) and self.container_image.runtime_packages:
238+
runtime_packages = " ".join(self.container_image.runtime_packages)
239+
env[RUNTIME_PACKAGES_ENV_NAME] = runtime_packages
240+
234241
return _get_container_definition(
235242
image=self.get_image(settings),
236243
resource_spec=self.resources,

flytekit/image_spec/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
from .default_builder import DefaultImageBuilder
1919
from .image_spec import ImageBuildEngine, ImageSpec
20+
from .noop_builder import NoOpBuilder
2021

2122
# Set this to a lower priority compared to `envd` to maintain backward compatibility
2223
ImageBuildEngine.register(DefaultImageBuilder.builder_type, DefaultImageBuilder(), priority=1)
24+
# Lower priority compared to Default.
25+
ImageBuildEngine.register(NoOpBuilder.builder_type, NoOpBuilder(), priority=0)

flytekit/image_spec/image_spec.py

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from dataclasses import asdict, dataclass
1212
from functools import cached_property, lru_cache
1313
from importlib import metadata
14-
from typing import Dict, List, Optional, Tuple, Union
14+
from typing import Any, Dict, List, Optional, Tuple, Union
1515

1616
import click
1717
import requests
@@ -30,50 +30,57 @@ class ImageSpec:
3030
"""
3131
This class is used to specify the docker image that will be used to run the task.
3232
33-
Args:
34-
name: name of the image.
35-
python_version: python version of the image. Use default python in the base image if None.
36-
builder: Type of plugin to build the image. Use envd by default.
37-
source_root: source root of the image.
38-
env: environment variables of the image.
39-
registry: registry of the image.
40-
packages: list of python packages to install.
41-
conda_packages: list of conda packages to install.
42-
conda_channels: list of conda channels.
43-
requirements: path to the requirements.txt file.
44-
apt_packages: list of apt packages to install.
45-
cuda: version of cuda to install.
46-
cudnn: version of cudnn to install.
47-
base_image: base image of the image.
48-
platform: Specify the target platforms for the build output (for example, windows/amd64 or linux/amd64,darwin/arm64
49-
pip_index: Specify the custom pip index url
50-
pip_extra_index_url: Specify one or more pip index urls as a list
51-
pip_secret_mounts: Specify a list of tuples to mount secret for pip install. Each tuple should contain the path to
33+
Attributes:
34+
name (str): Name of the image.
35+
python_version (str): Python version of the image. Use default python in the base image if None.
36+
builder (Optional[str]): Type of plugin to build the image. Use envd by default.
37+
source_root (Optional[str]): Source root of the image.
38+
env (Optional[Dict[str, str]]): Environment variables of the image.
39+
registry (Optional[str]): Registry of the image.
40+
packages (Optional[List[str]]): List of python packages to install.
41+
conda_packages (Optional[List[str]]): List of conda packages to install.
42+
conda_channels (Optional[List[str]]): List of conda channels.
43+
requirements (Optional[str]): Path to the requirements.txt file.
44+
apt_packages (Optional[List[str]]): List of apt packages to install.
45+
cuda (Optional[str]): Version of cuda to install.
46+
cudnn (Optional[str]): Version of cudnn to install.
47+
base_image (Optional[Union[str, 'ImageSpec']]): Base image of the image.
48+
platform (str): Specify the target platforms for the build output (for example, windows/amd64 or linux/amd64,darwin/arm64).
49+
pip_index (Optional[str]): Specify the custom pip index url.
50+
pip_extra_index_url (Optional[List[str]]): Specify one or more pip index urls as a list.
51+
pip_secret_mounts (Optional[List[Tuple[str, str]]]): Specify a list of tuples to mount secret for pip install. Each tuple should contain the path to
5252
the secret file and the mount path. For example, [(".gitconfig", "/etc/gitconfig")]. This is experimental and
5353
the interface may change in the future. Configuring this should not change the built image.
54-
pip_extra_args: Specify one or more extra pip install arguments as a space-delimited string
55-
registry_config: Specify the path to a JSON registry config file
56-
entrypoint: List of strings to overwrite the entrypoint of the base image with, set to [] to remove the entrypoint.
57-
commands: Command to run during the building process
58-
tag_format: Custom string format for image tag. The ImageSpec hash passed in as `spec_hash`. For example,
59-
to add a "dev" suffix to the image tag, set `tag_format="{spec_hash}-dev"`
60-
source_copy_mode: This option allows the user to specify which source files to copy from the local host, into the image.
54+
pip_extra_args (Optional[str]): Specify one or more extra pip install arguments as a space-delimited string.
55+
registry_config (Optional[str]): Specify the path to a JSON registry config file.
56+
entrypoint (Optional[List[str]]): List of strings to overwrite the entrypoint of the base image with, set to [] to remove the entrypoint.
57+
commands (Optional[List[str]]): Command to run during the building process.
58+
tag_format (Optional[str]): Custom string format for image tag. The ImageSpec hash passed in as `spec_hash`. For example,
59+
to add a "dev" suffix to the image tag, set `tag_format="{spec_hash}-dev"`.
60+
source_copy_mode (Optional[CopyFileDetection]): This option allows the user to specify which source files to copy from the local host, into the image.
6161
Not setting this option means to use the default flytekit behavior. The default behavior is:
6262
- if fast register is used, source files are not copied into the image (because they're already copied
6363
into the fast register tar layer).
6464
- if fast register is not used, then the LOADED_MODULES (aka 'auto') option is used to copy loaded
6565
Python files into the image.
66-
6766
If the option is set by the user, then that option is of course used.
68-
copy: List of files/directories to copy to /root. e.g. ["src/file1.txt", "src/file2.txt"]
69-
python_exec: Python executable to use for install packages
67+
copy (Optional[List[str]]): List of files/directories to copy to /root. e.g. ["src/file1.txt", "src/file2.txt"].
68+
python_exec (Optional[str]): Python executable to use for install packages.
69+
runtime_packages (Optional[List[str]]): List of packages to be installed during runtime. `runtime_packages` requires `pip` to be installed
70+
in your base image.
71+
- If you are using an ImageSpec as your base image, please include `pip` into your packages:
72+
`ImageSpec(..., packages=["pip"])`.
73+
- If you want to install runtime packages into a fixed base_image and not use an image builder, you can
74+
use `builder="noop"`: `ImageSpec(base_image="ghcr.io/name/my-custom-image", builder="noop").with_runtime_packages(["numpy"])`.
75+
builder_options (Optional[Dict[str, Any]]): Additional options for the builder. This is a dictionary that will be passed to the builder.
76+
The options are builder-specific and may not be supported by all builders.
7077
"""
7178

7279
name: str = "flytekit"
7380
python_version: str = None # Use default python in the base image if None.
7481
builder: Optional[str] = None
7582
source_root: Optional[str] = None # a.txt:auto
76-
env: Optional[typing.Dict[str, str]] = None
83+
env: Optional[Dict[str, str]] = None
7784
registry: Optional[str] = None
7885
packages: Optional[List[str]] = None
7986
conda_packages: Optional[List[str]] = None
@@ -95,6 +102,8 @@ class ImageSpec:
95102
source_copy_mode: Optional[CopyFileDetection] = None
96103
copy: Optional[List[str]] = None
97104
python_exec: Optional[str] = None
105+
runtime_packages: Optional[List[str]] = None
106+
builder_options: Optional[Dict[str, Any]] = None
98107

99108
def __post_init__(self):
100109
self.name = self.name.lower()
@@ -127,6 +136,7 @@ def __post_init__(self):
127136
"pip_extra_index_url",
128137
"entrypoint",
129138
"commands",
139+
"runtime_packages",
130140
]
131141
for parameter in parameters_str_list:
132142
attr = getattr(self, parameter)
@@ -145,6 +155,9 @@ def __post_init__(self):
145155
error_msg = "pip_secret_mounts must be a list of tuples of two strings or None"
146156
raise ValueError(error_msg)
147157

158+
if self.builder_options is not None and not isinstance(self.builder_options, dict):
159+
raise ValueError("builder_options must be a dictionary or None")
160+
148161
@cached_property
149162
def id(self) -> str:
150163
"""
@@ -160,7 +173,7 @@ def id(self) -> str:
160173
161174
:return: a unique identifier of the ImageSpec
162175
"""
163-
parameters_to_exclude = ["pip_secret_mounts", "builder"]
176+
parameters_to_exclude = ["pip_secret_mounts", "builder", "runtime_packages"]
164177
# Only get the non-None values in the ImageSpec to ensure the hash is consistent across different Flytekit versions.
165178
image_spec_dict = asdict(
166179
self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None and k not in parameters_to_exclude}
@@ -310,16 +323,30 @@ def exist(self) -> Optional[bool]:
310323
click.secho(f"Failed to check if the image exists with error:\n {e}", fg="red")
311324
return None
312325

313-
def _update_attribute(self, attr_name: str, values: Union[str, List[str]]) -> "ImageSpec":
326+
def _update_attribute(self, attr_name: str, values: Union[str, List[str], Dict[str, Any]]) -> "ImageSpec":
314327
"""
315-
Generic method to update a specified list attribute, either appending or extending.
328+
Generic method to update a specified attribute, handling strings, lists, and dictionaries.
316329
"""
317-
current_value = copy.deepcopy(getattr(self, attr_name)) or []
330+
current_value = copy.deepcopy(getattr(self, attr_name))
331+
332+
if current_value is None:
333+
if isinstance(values, dict):
334+
current_value = {}
335+
else:
336+
current_value = []
318337

319338
if isinstance(values, str):
339+
if not isinstance(current_value, list):
340+
raise TypeError(f"Cannot append string to non-list attribute {attr_name}")
320341
current_value.append(values)
321342
elif isinstance(values, list):
343+
if not isinstance(current_value, list):
344+
raise TypeError(f"Cannot extend non-list attribute {attr_name}")
322345
current_value.extend(values)
346+
elif isinstance(values, dict):
347+
if not isinstance(current_value, dict):
348+
raise TypeError(f"Cannot update non-dict attribute {attr_name}")
349+
current_value.update(values)
323350

324351
return dataclasses.replace(self, **{attr_name: current_value})
325352

@@ -358,6 +385,18 @@ def force_push(self) -> "ImageSpec":
358385

359386
return copied_image_spec
360387

388+
def with_runtime_packages(self, runtime_packages: List[str]) -> "ImageSpec":
389+
"""
390+
Builder that returns a new image spec with runtime packages. Dev packages will be installed during runtime.
391+
"""
392+
return self._update_attribute("runtime_packages", runtime_packages)
393+
394+
def with_builder_options(self, builder_options: Dict[str, Any]) -> "ImageSpec":
395+
"""
396+
Builder that returns a new image spec with additional builder options.
397+
"""
398+
return self._update_attribute("builder_options", builder_options)
399+
361400
@classmethod
362401
def from_env(cls, *, pinned_packages: Optional[List[str]] = None, **kwargs) -> "ImageSpec":
363402
"""Create ImageSpec with the environment's Python version and packages pinned to the ones in the environment."""
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from flytekit.image_spec.image_spec import ImageSpec, ImageSpecBuilder
2+
3+
4+
class NoOpBuilder(ImageSpecBuilder):
5+
"""Noop image builder."""
6+
7+
builder_type = "noop"
8+
9+
def build_image(self, image_spec: ImageSpec) -> str:
10+
if not isinstance(image_spec.base_image, str):
11+
msg = "base_image must be a string to use the noop image builder"
12+
raise ValueError(msg)
13+
14+
import click
15+
16+
click.secho(f"Using image: {image_spec.base_image}", fg="blue")
17+
return image_spec.base_image

pydoclint-errors-baseline.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,6 @@ flytekit/extras/tensorflow/record.py
173173
DOC603: Class `TFRecordDatasetConfig`: Class docstring attributes are different from actual class attributes. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Attributes in the class definition but not in the docstring: [buffer_size: Optional[int], compression_type: Optional[str], name: Optional[str], num_parallel_reads: Optional[int]]. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.)
174174
--------------------
175175
flytekit/image_spec/image_spec.py
176-
DOC601: Class `ImageSpec`: Class docstring contains fewer class attributes than actual class attributes. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.)
177-
DOC603: Class `ImageSpec`: Class docstring attributes are different from actual class attributes. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Attributes in the class definition but not in the docstring: [apt_packages: Optional[List[str]], base_image: Optional[Union[str, 'ImageSpec']], builder: Optional[str], commands: Optional[List[str]], conda_channels: Optional[List[str]], conda_packages: Optional[List[str]], copy: Optional[List[str]], cuda: Optional[str], cudnn: Optional[str], entrypoint: Optional[List[str]], env: Optional[typing.Dict[str, str]], name: str, packages: Optional[List[str]], pip_extra_args: Optional[str], pip_extra_index_url: Optional[List[str]], pip_index: Optional[str], pip_secret_mounts: Optional[List[Tuple[str, str]]], platform: str, python_exec: Optional[str], python_version: str, registry: Optional[str], registry_config: Optional[str], requirements: Optional[str], source_copy_mode: Optional[CopyFileDetection], source_root: Optional[str], tag_format: Optional[str]]. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.)
178176
DOC109: Method `ImageSpecBuilder.build_image`: The option `--arg-type-hints-in-docstring` is `True` but there are no type hints in the docstring arg list
179177
DOC110: Method `ImageSpecBuilder.build_image`: The option `--arg-type-hints-in-docstring` is `True` but not all args in the docstring arg list have type hints
180178
DOC105: Method `ImageSpecBuilder.build_image`: Argument names match, but type hints in these args do not match: image_spec

0 commit comments

Comments
 (0)