zykrix
Re-added phidata as a normal folder (final fix)
c96b98a
Raw
History Blame Contribute Delete
18.4 kB
from typing import Optional, Any, Dict, List
from phi.docker.api_client import DockerApiClient
from phi.docker.resource.base import DockerResource
from phi.cli.console import print_info, console
from phi.utils.log import logger
class DockerImage(DockerResource):
resource_type: str = "Image"
# Docker image name, usually as repo/image
name: str
# Docker image tag
tag: Optional[str] = None
# Path to the directory containing the Dockerfile
path: Optional[str] = None
# Path to the Dockerfile within the build context
dockerfile: Optional[str] = None
# Print the build log
print_build_log: bool = True
# Push the image to the registry. Similar to the docker push command.
push_image: bool = False
print_push_output: bool = False
# Use buildx for building images
use_buildx: bool = True
# Remove intermediate containers.
# The docker build command defaults to --rm=true,
# The docker api kept the old default of False to preserve backward compatibility
rm: Optional[bool] = True
# Always remove intermediate containers, even after unsuccessful builds
forcerm: Optional[bool] = None
# HTTP timeout
timeout: Optional[int] = None
# Downloads any updates to the FROM image in Dockerfiles
pull: Optional[bool] = None
# Skips docker cache when set to True
# i.e. rebuilds all layes of the image
skip_docker_cache: Optional[bool] = None
# A dictionary of build arguments
buildargs: Optional[Dict[str, Any]] = None
# A dictionary of limits applied to each container created by the build process. Valid keys:
# memory (int): set memory limit for build
# memswap (int): Total memory (memory + swap), -1 to disable swap
# cpushares (int): CPU shares (relative weight)
# cpusetcpus (str): CPUs in which to allow execution, e.g. "0-3", "0,1"
container_limits: Optional[Dict[str, Any]] = None
# Size of /dev/shm in bytes. The size must be greater than 0. If omitted the system uses 64MB
shmsize: Optional[int] = None
# A dictionary of labels to set on the image
labels: Optional[Dict[str, Any]] = None
# A list of images used for build cache resolution
cache_from: Optional[List[Any]] = None
# Name of the build-stage to build in a multi-stage Dockerfile
target: Optional[str] = None
# networking mode for the run commands during build
network_mode: Optional[str] = None
# Squash the resulting images layers into a single layer.
squash: Optional[bool] = None
# Extra hosts to add to /etc/hosts in building containers, as a mapping of hostname to IP address.
extra_hosts: Optional[Dict[str, Any]] = None
# Platform in the format os[/arch[/variant]].
platform: Optional[str] = None
# List of platforms to use for build, uses buildx_image if multi-platform build is enabled.
platforms: Optional[List[str]] = None
# Isolation technology used during build. Default: None.
isolation: Optional[str] = None
# If True, and if the docker client configuration file (~/.docker/config.json by default)
# contains a proxy configuration, the corresponding environment variables
# will be set in the container being built.
use_config_proxy: Optional[bool] = None
# Set skip_delete=True so that the image is not deleted when the `phi ws down` command is run
skip_delete: bool = True
image_build_id: Optional[str] = None
# Set use_cache to False so image is always built
use_cache: bool = False
def get_image_str(self) -> str:
if self.tag:
return f"{self.name}:{self.tag}"
return f"{self.name}:latest"
def get_resource_name(self) -> str:
return self.get_image_str()
def buildx(self, docker_client: Optional[DockerApiClient] = None) -> Optional[Any]:
"""Builds the image using buildx
Args:
docker_client: The DockerApiClient for the current cluster
Options: https://docs.docker.com/engine/reference/commandline/buildx_build/#options
"""
try:
import subprocess
tag = self.get_image_str()
nocache = self.skip_docker_cache or self.force
pull = self.pull or self.force
print_info(f"Building image: {tag}")
if self.path is not None:
print_info(f"\t path: {self.path}")
if self.dockerfile is not None:
print_info(f" dockerfile: {self.dockerfile}")
if self.platforms is not None:
print_info(f" platforms: {self.platforms}")
logger.debug(f"nocache: {nocache}")
logger.debug(f"pull: {pull}")
command = ["docker", "buildx", "build"]
# Add tag
command.extend(["--tag", tag])
# Add dockerfile option, if set
if self.dockerfile is not None:
command.extend(["--file", self.dockerfile])
# Add build arguments
if self.buildargs:
for key, value in self.buildargs.items():
command.extend(["--build-arg", f"{key}={value}"])
# Add no-cache option, if set
if nocache:
command.append("--no-cache")
if not self.rm:
command.append("--rm=false")
if self.platforms:
command.append("--platform={}".format(",".join(self.platforms)))
if self.pull:
command.append("--pull")
if self.push_image:
command.append("--push")
else:
command.append("--load")
# Add path
if self.path is not None:
command.append(self.path)
# Run the command
logger.debug("Running command: {}".format(" ".join(command)))
result = subprocess.run(command)
# Handling output and errors
if result.returncode == 0:
print_info("Docker image built successfully.")
return True
# _docker_client = docker_client or self.get_docker_client()
# return self._read(docker_client=_docker_client)
else:
logger.error("Error in building Docker image:")
return False
except Exception as e:
logger.error(e)
return None
def build_image(self, docker_client: DockerApiClient) -> Optional[Any]:
if self.platforms is not None or self.use_buildx:
logger.debug("Using buildx for multi-platform build")
return self.buildx(docker_client=docker_client)
from docker import DockerClient
from docker.errors import BuildError, APIError
from rich import box
from rich.live import Live
from rich.table import Table
print_info(f"Building image: {self.get_image_str()}")
nocache = self.skip_docker_cache or self.force
pull = self.pull or self.force
if self.path is not None:
print_info(f"\t path: {self.path}")
if self.dockerfile is not None:
print_info(f" dockerfile: {self.dockerfile}")
logger.debug(f"platform: {self.platform}")
logger.debug(f"nocache: {nocache}")
logger.debug(f"pull: {pull}")
last_status = None
last_build_log = None
build_log_output: List[Any] = []
build_step_progress: List[str] = []
build_log_to_show_on_error: List[str] = []
try:
_api_client: DockerClient = docker_client.api_client
build_stream = _api_client.api.build(
tag=self.get_image_str(),
path=self.path,
dockerfile=self.dockerfile,
nocache=nocache,
rm=self.rm,
forcerm=self.forcerm,
timeout=self.timeout,
pull=pull,
buildargs=self.buildargs,
container_limits=self.container_limits,
shmsize=self.shmsize,
labels=self.labels,
cache_from=self.cache_from,
target=self.target,
network_mode=self.network_mode,
squash=self.squash,
extra_hosts=self.extra_hosts,
platform=self.platform,
isolation=self.isolation,
use_config_proxy=self.use_config_proxy,
decode=True,
)
with Live(transient=True, console=console) as live_log:
for build_log in build_stream:
if build_log != last_build_log:
last_build_log = build_log
build_log_output.append(build_log)
build_status: str = build_log.get("status")
if build_status is not None:
_status = build_status.lower()
if _status in (
"waiting",
"downloading",
"extracting",
"verifying checksum",
"pulling fs layer",
):
continue
if build_status != last_status:
logger.debug(build_status)
last_status = build_status
if build_log.get("error", None) is not None:
live_log.stop()
logger.error(build_log_output[-50:])
logger.error(build_log["error"])
logger.error(f"Image build failed: {self.get_image_str()}")
return None
stream = build_log.get("stream", None)
if stream is None or stream == "\n":
continue
stream = stream.strip()
if "Step" in stream and self.print_build_log:
build_step_progress = []
print_info(stream)
else:
build_step_progress.append(stream)
if len(build_step_progress) > 10:
build_step_progress.pop(0)
build_log_to_show_on_error.append(stream)
if len(build_log_to_show_on_error) > 50:
build_log_to_show_on_error.pop(0)
if "error" in stream.lower():
print(stream)
live_log.stop()
# Render error table
error_table = Table(show_edge=False, show_header=False, show_lines=False)
for line in build_log_to_show_on_error:
error_table.add_row(line, style="dim")
error_table.add_row(stream, style="bold red")
console.print(error_table)
return None
if build_log.get("aux", None) is not None:
logger.debug("build_log['aux'] :{}".format(build_log["aux"]))
self.image_build_id = build_log.get("aux", {}).get("ID")
# Render table
table = Table(show_edge=False, show_header=False, show_lines=False)
for line in build_step_progress:
table.add_row(line, style="dim")
live_log.update(table)
if self.push_image:
print_info(f"Pushing {self.get_image_str()}")
with Live(transient=True, console=console) as live_log:
push_status = {}
last_push_progress = None
for push_output in _api_client.images.push(
repository=self.name,
tag=self.tag,
stream=True,
decode=True,
):
_id = push_output.get("id", None)
_status = push_output.get("status", None)
_progress = push_output.get("progress", None)
if _id is not None and _status is not None:
push_status[_id] = {
"status": _status,
"progress": _progress,
}
if push_output.get("error", None) is not None:
logger.error(push_output["error"])
logger.error(f"Push failed for {self.get_image_str()}")
logger.error("If you are using a private registry, make sure you are logged in")
return None
if self.print_push_output and push_output.get("status", None) in (
"Pushing",
"Pushed",
):
current_progress = push_output.get("progress", None)
if current_progress != last_push_progress:
print_info(current_progress)
last_push_progress = current_progress
if push_output.get("aux", {}).get("Size", 0) > 0:
print_info(f"Push complete: {push_output.get('aux', {})}")
# Render table
table = Table(box=box.ASCII2)
table.add_column("Layer", justify="center")
table.add_column("Status", justify="center")
table.add_column("Progress", justify="center")
for layer, layer_status in push_status.items():
table.add_row(
layer,
layer_status["status"],
layer_status["progress"],
style="dim",
)
live_log.update(table)
return self._read(docker_client)
except TypeError as type_error:
logger.error(type_error)
except BuildError as build_error:
logger.error(build_error)
except APIError as api_err:
logger.error(api_err)
except Exception as e:
logger.error(e)
return None
def _create(self, docker_client: DockerApiClient) -> bool:
"""Creates the image
Args:
docker_client: The DockerApiClient for the current cluster
"""
logger.debug("Creating: {}".format(self.get_resource_name()))
try:
image_object = self.build_image(docker_client)
if image_object is not None:
return True
return False
# if image_object is not None and isinstance(image_object, Image):
# logger.debug("Image built: {}".format(image_object))
# self.active_resource = image_object
# return True
except Exception as e:
logger.exception(e)
logger.error("Error while creating image: {}".format(e))
raise
def _read(self, docker_client: DockerApiClient) -> Any:
"""Returns an Image object if available
Args:
docker_client: The DockerApiClient for the current cluster
"""
from docker import DockerClient
from docker.models.images import Image
from docker.errors import ImageNotFound, NotFound
logger.debug("Reading: {}".format(self.get_image_str()))
try:
_api_client: DockerClient = docker_client.api_client
image_object: Optional[List[Image]] = _api_client.images.get(name=self.get_image_str())
if image_object is not None and isinstance(image_object, Image):
logger.debug("Image found: {}".format(image_object))
self.active_resource = image_object
return image_object
except (NotFound, ImageNotFound):
logger.debug(f"Image {self.tag} not found")
return None
def _update(self, docker_client: DockerApiClient) -> bool:
"""Updates the Image
Args:
docker_client: The DockerApiClient for the current cluster
"""
logger.debug("Updating: {}".format(self.get_resource_name()))
return self._create(docker_client=docker_client)
def _delete(self, docker_client: DockerApiClient) -> bool:
"""Deletes the Image
Args:
docker_client: The DockerApiClient for the current cluster
"""
from docker import DockerClient
from docker.models.images import Image
logger.debug("Deleting: {}".format(self.get_resource_name()))
image_object: Optional[Image] = self._read(docker_client)
# Return True if there is no image to delete
if image_object is None:
logger.debug("No image to delete")
return True
# Delete Image
try:
self.active_resource = None
logger.debug("Deleting image: {}".format(self.tag))
_api_client: DockerClient = docker_client.api_client
_api_client.images.remove(image=self.tag, force=True)
return True
except Exception as e:
logger.exception("Error while deleting image: {}".format(e))
return False
def create(self, docker_client: DockerApiClient) -> bool:
# If self.force then always create container
if not self.force:
# If use_cache is True and image is active then return True
if self.use_cache and self.is_active(docker_client=docker_client):
print_info(f"{self.get_resource_type()}: {self.get_resource_name()} already exists")
return True
resource_created = self._create(docker_client=docker_client)
if resource_created:
print_info(f"{self.get_resource_type()}: {self.get_resource_name()} created")
return True
logger.error(f"Failed to create {self.get_resource_type()}: {self.get_resource_name()}")
return False