Image Builders
zenml.image_builders
special
Image builders allow you to build container images.
base_image_builder
Base class for all ZenML image builders.
BaseImageBuilder (StackComponent, ABC)
Base class for all ZenML image builders.
Source code in zenml/image_builders/base_image_builder.py
class BaseImageBuilder(StackComponent, ABC):
"""Base class for all ZenML image builders."""
@property
def config(self) -> BaseImageBuilderConfig:
"""The stack component configuration.
Returns:
The configuration.
"""
return cast(BaseImageBuilderConfig, self._config)
@property
def build_context_class(self) -> Type["BuildContext"]:
"""Build context class to use.
The default build context class creates a build context that works
for the Docker daemon. Override this method if your image builder
requires a custom context.
Returns:
The build context class.
"""
from zenml.image_builders import BuildContext
return BuildContext
@abstractmethod
def build(
self,
image_name: str,
build_context: "BuildContext",
docker_build_options: Dict[str, Any],
container_registry: Optional["BaseContainerRegistry"] = None,
) -> str:
"""Builds a Docker image.
If a container registry is passed, the image will be pushed to that
registry.
Args:
image_name: Name of the image to build.
build_context: The build context to use for the image.
docker_build_options: Docker build options.
container_registry: Optional container registry to push to.
Returns:
The Docker image repo digest or name.
"""
build_context_class: Type[BuildContext]
property
readonly
Build context class to use.
The default build context class creates a build context that works for the Docker daemon. Override this method if your image builder requires a custom context.
Returns:
Type | Description |
---|---|
Type[BuildContext] |
The build context class. |
config: BaseImageBuilderConfig
property
readonly
The stack component configuration.
Returns:
Type | Description |
---|---|
BaseImageBuilderConfig |
The configuration. |
build(self, image_name, build_context, docker_build_options, container_registry=None)
Builds a Docker image.
If a container registry is passed, the image will be pushed to that registry.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image_name |
str |
Name of the image to build. |
required |
build_context |
BuildContext |
The build context to use for the image. |
required |
docker_build_options |
Dict[str, Any] |
Docker build options. |
required |
container_registry |
Optional[BaseContainerRegistry] |
Optional container registry to push to. |
None |
Returns:
Type | Description |
---|---|
str |
The Docker image repo digest or name. |
Source code in zenml/image_builders/base_image_builder.py
@abstractmethod
def build(
self,
image_name: str,
build_context: "BuildContext",
docker_build_options: Dict[str, Any],
container_registry: Optional["BaseContainerRegistry"] = None,
) -> str:
"""Builds a Docker image.
If a container registry is passed, the image will be pushed to that
registry.
Args:
image_name: Name of the image to build.
build_context: The build context to use for the image.
docker_build_options: Docker build options.
container_registry: Optional container registry to push to.
Returns:
The Docker image repo digest or name.
"""
BaseImageBuilderConfig (StackComponentConfig)
pydantic-model
Base config for image builders.
Source code in zenml/image_builders/base_image_builder.py
class BaseImageBuilderConfig(StackComponentConfig):
"""Base config for image builders."""
BaseImageBuilderFlavor (Flavor, ABC)
Base class for all ZenML image builder flavors.
Source code in zenml/image_builders/base_image_builder.py
class BaseImageBuilderFlavor(Flavor, ABC):
"""Base class for all ZenML image builder flavors."""
@property
def type(self) -> StackComponentType:
"""Returns the flavor type.
Returns:
The flavor type.
"""
return StackComponentType.IMAGE_BUILDER
@property
def config_class(self) -> Type[BaseImageBuilderConfig]:
"""Config class.
Returns:
The config class.
"""
return BaseImageBuilderConfig
@property
def implementation_class(self) -> Type[BaseImageBuilder]:
"""Implementation class.
Returns:
The implementation class.
"""
return BaseImageBuilder
config_class: Type[zenml.image_builders.base_image_builder.BaseImageBuilderConfig]
property
readonly
Config class.
Returns:
Type | Description |
---|---|
Type[zenml.image_builders.base_image_builder.BaseImageBuilderConfig] |
The config class. |
implementation_class: Type[zenml.image_builders.base_image_builder.BaseImageBuilder]
property
readonly
Implementation class.
Returns:
Type | Description |
---|---|
Type[zenml.image_builders.base_image_builder.BaseImageBuilder] |
The implementation class. |
type: StackComponentType
property
readonly
Returns the flavor type.
Returns:
Type | Description |
---|---|
StackComponentType |
The flavor type. |
build_context
Image build context.
BuildContext
Image build context.
This class is responsible for creating an archive of the files needed to build a container image.
Source code in zenml/image_builders/build_context.py
class BuildContext:
"""Image build context.
This class is responsible for creating an archive of the files needed to
build a container image.
"""
def __init__(
self,
root: Optional[str] = None,
dockerignore_file: Optional[str] = None,
) -> None:
"""Initializes a build context.
Args:
root: Optional root directory for the build context.
dockerignore_file: Optional path to a dockerignore file. If not
given, a file called `.dockerignore` in the build context root
directory will be used instead if it exists.
"""
self._root = root
self._dockerignore_file = dockerignore_file
self._extra_files: Dict[str, str] = {}
@property
def dockerignore_file(self) -> Optional[str]:
"""The dockerignore file to use.
Returns:
Path to the dockerignore file to use.
"""
if self._dockerignore_file:
return self._dockerignore_file
if self._root:
default_dockerignore_path = os.path.join(
self._root, ".dockerignore"
)
if fileio.exists(default_dockerignore_path):
return default_dockerignore_path
return None
def add_file(self, source: str, destination: str) -> None:
"""Adds a file to the build context.
Args:
source: The source of the file to add. This can either be a path
or the file content.
destination: The path inside the build context where the file
should be added.
"""
if fileio.exists(source):
with fileio.open(source) as f:
self._extra_files[destination] = f.read()
else:
self._extra_files[destination] = source
def add_directory(self, source: str, destination: str) -> None:
"""Adds a directory to the build context.
Args:
source: Path to the directory.
destination: The path inside the build context where the directory
should be added.
Raises:
ValueError: If `source` does not point to a directory.
"""
if not fileio.isdir(source):
raise ValueError(
f"Can't add directory {source} to the build context as it "
"does not exist or is not a directory."
)
for dir, _, files in fileio.walk(source):
dir_path = Path(fileio.convert_to_str(dir))
for file_name in files:
file_name = fileio.convert_to_str(file_name)
file_source = dir_path / file_name
file_destination = (
Path(destination)
/ dir_path.relative_to(source)
/ file_name
)
with file_source.open("r") as f:
self._extra_files[file_destination.as_posix()] = f.read()
def write_archive(self, output_file: IO[bytes], gzip: bool = True) -> None:
"""Writes an archive of the build context to the given file.
Args:
output_file: The file to write the archive to.
gzip: Whether to use `gzip` to compress the file.
"""
from docker.utils import build as docker_build_utils
files = self._get_files()
extra_files = self._get_extra_files()
context_archive = docker_build_utils.create_archive(
fileobj=output_file,
root=self._root,
files=sorted(files),
gzip=gzip,
extra_files=extra_files,
)
build_context_size = os.path.getsize(context_archive.name)
if (
self._root
and build_context_size > 50 * 1024 * 1024
and not self.dockerignore_file
):
# The build context exceeds 50MiB and we didn't find any excludes
# in dockerignore files -> remind to specify a .dockerignore file
logger.warning(
"Build context size for docker image: `%s`. If you believe this is "
"unreasonably large, make sure to include a `.dockerignore` file "
"at the root of your build context `%s` or specify a custom file "
"in the Docker configuration when defining your pipeline.",
string_utils.get_human_readable_filesize(build_context_size),
os.path.join(self._root, ".dockerignore"),
)
def _get_files(self) -> Set[str]:
"""Gets all non-ignored files in the build context root directory.
Returns:
All build context files.
"""
if self._root:
exclude_patterns = self._get_exclude_patterns()
from docker.utils import build as docker_build_utils
return cast(
Set[str],
docker_build_utils.exclude_paths(
self._root, patterns=exclude_patterns
),
)
else:
return set()
def _get_extra_files(self) -> List[Tuple[str, str]]:
"""Gets all extra files of the build context.
Returns:
A tuple (path, file_content) for all extra files in the build
context.
"""
return list(self._extra_files.items())
def _get_exclude_patterns(self) -> List[str]:
"""Gets all exclude patterns from the dockerignore file.
Returns:
The exclude patterns from the dockerignore file.
"""
dockerignore = self.dockerignore_file
if dockerignore:
return self._parse_dockerignore(dockerignore)
else:
logger.info(
"No `.dockerignore` found, including all files inside build "
"context.",
)
return []
@staticmethod
def _parse_dockerignore(dockerignore_path: str) -> List[str]:
"""Parses a dockerignore file and returns a list of patterns to ignore.
Args:
dockerignore_path: Path to the dockerignore file.
Returns:
List of patterns to ignore.
"""
try:
file_content = io_utils.read_file_contents_as_string(
dockerignore_path
)
except FileNotFoundError:
logger.warning(
"Unable to find dockerignore file at path '%s'.",
dockerignore_path,
)
return []
exclude_patterns = []
for line in file_content.split("\n"):
line = line.strip()
if line and not line.startswith("#"):
exclude_patterns.append(line)
return exclude_patterns
dockerignore_file: Optional[str]
property
readonly
The dockerignore file to use.
Returns:
Type | Description |
---|---|
Optional[str] |
Path to the dockerignore file to use. |
__init__(self, root=None, dockerignore_file=None)
special
Initializes a build context.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
root |
Optional[str] |
Optional root directory for the build context. |
None |
dockerignore_file |
Optional[str] |
Optional path to a dockerignore file. If not
given, a file called |
None |
Source code in zenml/image_builders/build_context.py
def __init__(
self,
root: Optional[str] = None,
dockerignore_file: Optional[str] = None,
) -> None:
"""Initializes a build context.
Args:
root: Optional root directory for the build context.
dockerignore_file: Optional path to a dockerignore file. If not
given, a file called `.dockerignore` in the build context root
directory will be used instead if it exists.
"""
self._root = root
self._dockerignore_file = dockerignore_file
self._extra_files: Dict[str, str] = {}
add_directory(self, source, destination)
Adds a directory to the build context.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str |
Path to the directory. |
required |
destination |
str |
The path inside the build context where the directory should be added. |
required |
Exceptions:
Type | Description |
---|---|
ValueError |
If |
Source code in zenml/image_builders/build_context.py
def add_directory(self, source: str, destination: str) -> None:
"""Adds a directory to the build context.
Args:
source: Path to the directory.
destination: The path inside the build context where the directory
should be added.
Raises:
ValueError: If `source` does not point to a directory.
"""
if not fileio.isdir(source):
raise ValueError(
f"Can't add directory {source} to the build context as it "
"does not exist or is not a directory."
)
for dir, _, files in fileio.walk(source):
dir_path = Path(fileio.convert_to_str(dir))
for file_name in files:
file_name = fileio.convert_to_str(file_name)
file_source = dir_path / file_name
file_destination = (
Path(destination)
/ dir_path.relative_to(source)
/ file_name
)
with file_source.open("r") as f:
self._extra_files[file_destination.as_posix()] = f.read()
add_file(self, source, destination)
Adds a file to the build context.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
str |
The source of the file to add. This can either be a path or the file content. |
required |
destination |
str |
The path inside the build context where the file should be added. |
required |
Source code in zenml/image_builders/build_context.py
def add_file(self, source: str, destination: str) -> None:
"""Adds a file to the build context.
Args:
source: The source of the file to add. This can either be a path
or the file content.
destination: The path inside the build context where the file
should be added.
"""
if fileio.exists(source):
with fileio.open(source) as f:
self._extra_files[destination] = f.read()
else:
self._extra_files[destination] = source
write_archive(self, output_file, gzip=True)
Writes an archive of the build context to the given file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
output_file |
IO[bytes] |
The file to write the archive to. |
required |
gzip |
bool |
Whether to use |
True |
Source code in zenml/image_builders/build_context.py
def write_archive(self, output_file: IO[bytes], gzip: bool = True) -> None:
"""Writes an archive of the build context to the given file.
Args:
output_file: The file to write the archive to.
gzip: Whether to use `gzip` to compress the file.
"""
from docker.utils import build as docker_build_utils
files = self._get_files()
extra_files = self._get_extra_files()
context_archive = docker_build_utils.create_archive(
fileobj=output_file,
root=self._root,
files=sorted(files),
gzip=gzip,
extra_files=extra_files,
)
build_context_size = os.path.getsize(context_archive.name)
if (
self._root
and build_context_size > 50 * 1024 * 1024
and not self.dockerignore_file
):
# The build context exceeds 50MiB and we didn't find any excludes
# in dockerignore files -> remind to specify a .dockerignore file
logger.warning(
"Build context size for docker image: `%s`. If you believe this is "
"unreasonably large, make sure to include a `.dockerignore` file "
"at the root of your build context `%s` or specify a custom file "
"in the Docker configuration when defining your pipeline.",
string_utils.get_human_readable_filesize(build_context_size),
os.path.join(self._root, ".dockerignore"),
)
local_image_builder
Local Docker image builder implementation.
LocalImageBuilder (BaseImageBuilder)
Local image builder implementation.
Source code in zenml/image_builders/local_image_builder.py
class LocalImageBuilder(BaseImageBuilder):
"""Local image builder implementation."""
@property
def config(self) -> LocalImageBuilderConfig:
"""The stack component configuration.
Returns:
The configuration.
"""
return cast(LocalImageBuilderConfig, self._config)
@staticmethod
def _check_prerequisites() -> None:
"""Checks that all prerequisites are installed.
Raises:
RuntimeError: If any of the prerequisites are not installed or
running.
"""
if not shutil.which("docker"):
raise RuntimeError(
"`docker` is required to run the local image builder."
)
if not docker_utils.check_docker():
raise RuntimeError(
"Unable to connect to the Docker daemon. There are two "
"common causes for this:\n"
"1) The Docker daemon isn't running.\n"
"2) The Docker client isn't configured correctly. The client "
"loads its configuration from the following file: "
"$HOME/.docker/config.json. If your configuration file is in a "
"different location, set it using the `DOCKER_CONFIG` "
"environment variable."
)
def build(
self,
image_name: str,
build_context: "BuildContext",
docker_build_options: Optional[Dict[str, Any]] = None,
container_registry: Optional["BaseContainerRegistry"] = None,
) -> str:
"""Builds and optionally pushes an image using the local Docker client.
Args:
image_name: Name of the image to build and push.
build_context: The build context to use for the image.
docker_build_options: Docker build options.
container_registry: Optional container registry to push to.
Returns:
The Docker image repo digest.
"""
self._check_prerequisites()
docker_client = DockerClient.from_env()
with tempfile.TemporaryFile(mode="w+b") as f:
build_context.write_archive(f)
# We use the client api directly here, so we can stream the logs
output_stream = docker_client.images.client.api.build(
fileobj=f,
custom_context=True,
tag=image_name,
**(docker_build_options or {}),
)
docker_utils._process_stream(output_stream)
if container_registry:
return container_registry.push_image(image_name)
else:
return image_name
config: LocalImageBuilderConfig
property
readonly
The stack component configuration.
Returns:
Type | Description |
---|---|
LocalImageBuilderConfig |
The configuration. |
build(self, image_name, build_context, docker_build_options=None, container_registry=None)
Builds and optionally pushes an image using the local Docker client.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image_name |
str |
Name of the image to build and push. |
required |
build_context |
BuildContext |
The build context to use for the image. |
required |
docker_build_options |
Optional[Dict[str, Any]] |
Docker build options. |
None |
container_registry |
Optional[BaseContainerRegistry] |
Optional container registry to push to. |
None |
Returns:
Type | Description |
---|---|
str |
The Docker image repo digest. |
Source code in zenml/image_builders/local_image_builder.py
def build(
self,
image_name: str,
build_context: "BuildContext",
docker_build_options: Optional[Dict[str, Any]] = None,
container_registry: Optional["BaseContainerRegistry"] = None,
) -> str:
"""Builds and optionally pushes an image using the local Docker client.
Args:
image_name: Name of the image to build and push.
build_context: The build context to use for the image.
docker_build_options: Docker build options.
container_registry: Optional container registry to push to.
Returns:
The Docker image repo digest.
"""
self._check_prerequisites()
docker_client = DockerClient.from_env()
with tempfile.TemporaryFile(mode="w+b") as f:
build_context.write_archive(f)
# We use the client api directly here, so we can stream the logs
output_stream = docker_client.images.client.api.build(
fileobj=f,
custom_context=True,
tag=image_name,
**(docker_build_options or {}),
)
docker_utils._process_stream(output_stream)
if container_registry:
return container_registry.push_image(image_name)
else:
return image_name
LocalImageBuilderConfig (BaseImageBuilderConfig)
pydantic-model
Local image builder configuration.
Source code in zenml/image_builders/local_image_builder.py
class LocalImageBuilderConfig(BaseImageBuilderConfig):
"""Local image builder configuration."""
@property
def is_local(self) -> bool:
"""Checks if this stack component is running locally.
Returns:
True.
"""
return True
is_local: bool
property
readonly
Checks if this stack component is running locally.
Returns:
Type | Description |
---|---|
bool |
True. |
LocalImageBuilderFlavor (BaseImageBuilderFlavor)
Local image builder flavor.
Source code in zenml/image_builders/local_image_builder.py
class LocalImageBuilderFlavor(BaseImageBuilderFlavor):
"""Local image builder flavor."""
@property
def name(self) -> str:
"""The flavor name.
Returns:
The flavor name.
"""
return "local"
@property
def config_class(self) -> Type[LocalImageBuilderConfig]:
"""Config class.
Returns:
The config class.
"""
return LocalImageBuilderConfig
@property
def implementation_class(self) -> Type[LocalImageBuilder]:
"""Implementation class.
Returns:
The implementation class.
"""
return LocalImageBuilder
config_class: Type[zenml.image_builders.local_image_builder.LocalImageBuilderConfig]
property
readonly
Config class.
Returns:
Type | Description |
---|---|
Type[zenml.image_builders.local_image_builder.LocalImageBuilderConfig] |
The config class. |
implementation_class: Type[zenml.image_builders.local_image_builder.LocalImageBuilder]
property
readonly
Implementation class.
Returns:
Type | Description |
---|---|
Type[zenml.image_builders.local_image_builder.LocalImageBuilder] |
The implementation class. |
name: str
property
readonly
The flavor name.
Returns:
Type | Description |
---|---|
str |
The flavor name. |