Spaces:
Runtime error
Runtime error
from typing import Optional, Dict, Any, Union, List, TYPE_CHECKING | |
from phi.app.base import AppBase | |
from phi.app.context import ContainerContext | |
from phi.docker.app.context import DockerBuildContext | |
from phi.utils.log import logger | |
if TYPE_CHECKING: | |
from phi.docker.resource.base import DockerResource | |
class DockerApp(AppBase): | |
# -*- Workspace Configuration | |
# Path to the workspace directory inside the container | |
workspace_dir_container_path: str = "/usr/local/app" | |
# Mount the workspace directory from host machine to the container | |
mount_workspace: bool = False | |
# -*- App Volume | |
# Create a volume for container storage | |
create_volume: bool = False | |
# If volume_dir is provided, mount this directory RELATIVE to the workspace_root | |
# from the host machine to the volume_container_path | |
volume_dir: Optional[str] = None | |
# Otherwise, mount a volume named volume_name to the container | |
# If volume_name is not provided, use {app-name}-volume | |
volume_name: Optional[str] = None | |
# Path to mount the volume inside the container | |
volume_container_path: str = "/mnt/app" | |
# -*- Resources Volume | |
# Mount a read-only directory from host machine to the container | |
mount_resources: bool = False | |
# Resources directory relative to the workspace_root | |
resources_dir: str = "workspace/resources" | |
# Path to mount the resources_dir | |
resources_dir_container_path: str = "/mnt/resources" | |
# -*- Container Configuration | |
container_name: Optional[str] = None | |
container_labels: Optional[Dict[str, str]] = None | |
# Run container in the background and return a Container object | |
container_detach: bool = True | |
# Enable auto-removal of the container on daemon side when the container’s process exits | |
container_auto_remove: bool = True | |
# Remove the container when it has finished running. Default: True | |
container_remove: bool = True | |
# Username or UID to run commands as inside the container | |
container_user: Optional[Union[str, int]] = None | |
# Keep STDIN open even if not attached | |
container_stdin_open: bool = True | |
# Return logs from STDOUT when container_detach=False | |
container_stdout: Optional[bool] = True | |
# Return logs from STDERR when container_detach=False | |
container_stderr: Optional[bool] = True | |
container_tty: bool = True | |
# Specify a test to perform to check that the container is healthy | |
container_healthcheck: Optional[Dict[str, Any]] = None | |
# Optional hostname for the container | |
container_hostname: Optional[str] = None | |
# Platform in the format os[/arch[/variant]] | |
container_platform: Optional[str] = None | |
# Path to the working directory | |
container_working_dir: Optional[str] = None | |
# Restart the container when it exits. Configured as a dictionary with keys: | |
# Name: One of on-failure, or always. | |
# MaximumRetryCount: Number of times to restart the container on failure. | |
# For example: {"Name": "on-failure", "MaximumRetryCount": 5} | |
container_restart_policy: Optional[Dict[str, Any]] = None | |
# Add volumes to DockerContainer | |
# container_volumes is a dictionary which adds the volumes to mount | |
# inside the container. The key is either the host path or a volume name, | |
# and the value is a dictionary with 2 keys: | |
# bind - The path to mount the volume inside the container | |
# mode - Either rw to mount the volume read/write, or ro to mount it read-only. | |
# For example: | |
# { | |
# '/home/user1/': {'bind': '/mnt/vol2', 'mode': 'rw'}, | |
# '/var/www': {'bind': '/mnt/vol1', 'mode': 'ro'} | |
# } | |
container_volumes: Optional[Dict[str, dict]] = None | |
# Add ports to DockerContainer | |
# The keys of the dictionary are the ports to bind inside the container, | |
# either as an integer or a string in the form port/protocol, where the protocol is either tcp, udp. | |
# The values of the dictionary are the corresponding ports to open on the host, which can be either: | |
# - The port number, as an integer. | |
# For example, {'2222/tcp': 3333} will expose port 2222 inside the container as port 3333 on the host. | |
# - None, to assign a random host port. For example, {'2222/tcp': None}. | |
# - A tuple of (address, port) if you want to specify the host interface. | |
# For example, {'1111/tcp': ('127.0.0.1', 1111)}. | |
# - A list of integers, if you want to bind multiple host ports to a single container port. | |
# For example, {'1111/tcp': [1234, 4567]}. | |
container_ports: Optional[Dict[str, Any]] = None | |
def get_container_name(self) -> str: | |
return self.container_name or self.get_app_name() | |
def get_container_context(self) -> Optional[ContainerContext]: | |
logger.debug("Building ContainerContext") | |
if self.container_context is not None: | |
return self.container_context | |
workspace_name = self.workspace_name | |
if workspace_name is None: | |
raise Exception("Could not determine workspace_name") | |
workspace_root_in_container = self.workspace_dir_container_path | |
if workspace_root_in_container is None: | |
raise Exception("Could not determine workspace_root in container") | |
workspace_parent_paths = workspace_root_in_container.split("/")[0:-1] | |
workspace_parent_in_container = "/".join(workspace_parent_paths) | |
self.container_context = ContainerContext( | |
workspace_name=workspace_name, | |
workspace_root=workspace_root_in_container, | |
workspace_parent=workspace_parent_in_container, | |
) | |
if self.workspace_settings is not None and self.workspace_settings.scripts_dir is not None: | |
self.container_context.scripts_dir = f"{workspace_root_in_container}/{self.workspace_settings.scripts_dir}" | |
if self.workspace_settings is not None and self.workspace_settings.storage_dir is not None: | |
self.container_context.storage_dir = f"{workspace_root_in_container}/{self.workspace_settings.storage_dir}" | |
if self.workspace_settings is not None and self.workspace_settings.workflows_dir is not None: | |
self.container_context.workflows_dir = ( | |
f"{workspace_root_in_container}/{self.workspace_settings.workflows_dir}" | |
) | |
if self.workspace_settings is not None and self.workspace_settings.workspace_dir is not None: | |
self.container_context.workspace_dir = ( | |
f"{workspace_root_in_container}/{self.workspace_settings.workspace_dir}" | |
) | |
if self.workspace_settings is not None and self.workspace_settings.ws_schema is not None: | |
self.container_context.workspace_schema = self.workspace_settings.ws_schema | |
if self.requirements_file is not None: | |
self.container_context.requirements_file = f"{workspace_root_in_container}/{self.requirements_file}" | |
return self.container_context | |
def get_container_env(self, container_context: ContainerContext) -> Dict[str, str]: | |
from phi.constants import ( | |
PHI_RUNTIME_ENV_VAR, | |
PYTHONPATH_ENV_VAR, | |
REQUIREMENTS_FILE_PATH_ENV_VAR, | |
SCRIPTS_DIR_ENV_VAR, | |
STORAGE_DIR_ENV_VAR, | |
WORKFLOWS_DIR_ENV_VAR, | |
WORKSPACE_DIR_ENV_VAR, | |
WORKSPACE_HASH_ENV_VAR, | |
WORKSPACE_ID_ENV_VAR, | |
WORKSPACE_ROOT_ENV_VAR, | |
) | |
# Container Environment | |
container_env: Dict[str, str] = self.container_env or {} | |
container_env.update( | |
{ | |
"INSTALL_REQUIREMENTS": str(self.install_requirements), | |
"MOUNT_RESOURCES": str(self.mount_resources), | |
"MOUNT_WORKSPACE": str(self.mount_workspace), | |
"PRINT_ENV_ON_LOAD": str(self.print_env_on_load), | |
"RESOURCES_DIR_CONTAINER_PATH": str(self.resources_dir_container_path), | |
PHI_RUNTIME_ENV_VAR: "docker", | |
REQUIREMENTS_FILE_PATH_ENV_VAR: container_context.requirements_file or "", | |
SCRIPTS_DIR_ENV_VAR: container_context.scripts_dir or "", | |
STORAGE_DIR_ENV_VAR: container_context.storage_dir or "", | |
WORKFLOWS_DIR_ENV_VAR: container_context.workflows_dir or "", | |
WORKSPACE_DIR_ENV_VAR: container_context.workspace_dir or "", | |
WORKSPACE_ROOT_ENV_VAR: container_context.workspace_root or "", | |
} | |
) | |
try: | |
if container_context.workspace_schema is not None: | |
if container_context.workspace_schema.id_workspace is not None: | |
container_env[WORKSPACE_ID_ENV_VAR] = str(container_context.workspace_schema.id_workspace) or "" | |
if container_context.workspace_schema.ws_hash is not None: | |
container_env[WORKSPACE_HASH_ENV_VAR] = container_context.workspace_schema.ws_hash | |
except Exception: | |
pass | |
if self.set_python_path: | |
python_path = self.python_path | |
if python_path is None: | |
python_path = container_context.workspace_root | |
if self.mount_resources and self.resources_dir_container_path is not None: | |
python_path = "{}:{}".format(python_path, self.resources_dir_container_path) | |
if self.add_python_paths is not None: | |
python_path = "{}:{}".format(python_path, ":".join(self.add_python_paths)) | |
if python_path is not None: | |
container_env[PYTHONPATH_ENV_VAR] = python_path | |
# Set aws region and profile | |
self.set_aws_env_vars(env_dict=container_env) | |
# Update the container env using env_file | |
env_data_from_file = self.get_env_file_data() | |
if env_data_from_file is not None: | |
container_env.update({k: str(v) for k, v in env_data_from_file.items() if v is not None}) | |
# Update the container env using secrets_file | |
secret_data_from_file = self.get_secret_file_data() | |
if secret_data_from_file is not None: | |
container_env.update({k: str(v) for k, v in secret_data_from_file.items() if v is not None}) | |
# Update the container env with user provided env_vars | |
# this overwrites any existing variables with the same key | |
if self.env_vars is not None and isinstance(self.env_vars, dict): | |
container_env.update({k: str(v) for k, v in self.env_vars.items() if v is not None}) | |
# logger.debug("Container Environment: {}".format(container_env)) | |
return container_env | |
def get_container_volumes(self, container_context: ContainerContext) -> Dict[str, dict]: | |
from phi.utils.defaults import get_default_volume_name | |
if self.workspace_root is None: | |
logger.error("Invalid workspace_root") | |
return {} | |
# container_volumes is a dictionary which configures the volumes to mount | |
# inside the container. The key is either the host path or a volume name, | |
# and the value is a dictionary with 2 keys: | |
# bind - The path to mount the volume inside the container | |
# mode - Either rw to mount the volume read/write, or ro to mount it read-only. | |
# For example: | |
# { | |
# '/home/user1/': {'bind': '/mnt/vol2', 'mode': 'rw'}, | |
# '/var/www': {'bind': '/mnt/vol1', 'mode': 'ro'} | |
# } | |
container_volumes = self.container_volumes or {} | |
# Create Workspace Volume | |
if self.mount_workspace: | |
workspace_root_in_container = container_context.workspace_root | |
workspace_root_on_host = str(self.workspace_root) | |
logger.debug(f"Mounting: {workspace_root_on_host}") | |
logger.debug(f" to: {workspace_root_in_container}") | |
container_volumes[workspace_root_on_host] = { | |
"bind": workspace_root_in_container, | |
"mode": "rw", | |
} | |
# Create App Volume | |
if self.create_volume: | |
volume_host = self.volume_name or get_default_volume_name(self.get_app_name()) | |
if self.volume_dir is not None: | |
volume_host = str(self.workspace_root.joinpath(self.volume_dir)) | |
logger.debug(f"Mounting: {volume_host}") | |
logger.debug(f" to: {self.volume_container_path}") | |
container_volumes[volume_host] = { | |
"bind": self.volume_container_path, | |
"mode": "rw", | |
} | |
# Create Resources Volume | |
if self.mount_resources: | |
resources_dir_path = str(self.workspace_root.joinpath(self.resources_dir)) | |
logger.debug(f"Mounting: {resources_dir_path}") | |
logger.debug(f" to: {self.resources_dir_container_path}") | |
container_volumes[resources_dir_path] = { | |
"bind": self.resources_dir_container_path, | |
"mode": "ro", | |
} | |
return container_volumes | |
def get_container_ports(self) -> Dict[str, int]: | |
# container_ports is a dictionary which configures the ports to bind | |
# inside the container. The key is the port to bind inside the container | |
# either as an integer or a string in the form port/protocol | |
# and the value is the corresponding port to open on the host. | |
# For example: | |
# {'2222/tcp': 3333} will expose port 2222 inside the container as port 3333 on the host. | |
container_ports: Dict[str, int] = self.container_ports or {} | |
if self.open_port: | |
_container_port = self.container_port or self.port_number | |
_host_port = self.host_port or self.port_number | |
container_ports[str(_container_port)] = _host_port | |
return container_ports | |
def get_container_command(self) -> Optional[List[str]]: | |
if isinstance(self.command, str): | |
return self.command.strip().split(" ") | |
return self.command | |
def build_resources(self, build_context: DockerBuildContext) -> List["DockerResource"]: | |
from phi.docker.resource.base import DockerResource | |
from phi.docker.resource.network import DockerNetwork | |
from phi.docker.resource.container import DockerContainer | |
logger.debug(f"------------ Building {self.get_app_name()} ------------") | |
# -*- Get Container Context | |
container_context: Optional[ContainerContext] = self.get_container_context() | |
if container_context is None: | |
raise Exception("Could not build ContainerContext") | |
logger.debug(f"ContainerContext: {container_context.model_dump_json(indent=2)}") | |
# -*- Get Container Environment | |
container_env: Dict[str, str] = self.get_container_env(container_context=container_context) | |
# -*- Get Container Volumes | |
container_volumes = self.get_container_volumes(container_context=container_context) | |
# -*- Get Container Ports | |
container_ports: Dict[str, int] = self.get_container_ports() | |
# -*- Get Container Command | |
container_cmd: Optional[List[str]] = self.get_container_command() | |
if container_cmd: | |
logger.debug("Command: {}".format(" ".join(container_cmd))) | |
# -*- Build the DockerContainer for this App | |
docker_container = DockerContainer( | |
name=self.get_container_name(), | |
image=self.get_image_str(), | |
entrypoint=self.entrypoint, | |
command=" ".join(container_cmd) if container_cmd is not None else None, | |
detach=self.container_detach, | |
auto_remove=self.container_auto_remove if not self.debug_mode else False, | |
remove=self.container_remove if not self.debug_mode else False, | |
healthcheck=self.container_healthcheck, | |
hostname=self.container_hostname, | |
labels=self.container_labels, | |
environment=container_env, | |
network=build_context.network, | |
platform=self.container_platform, | |
ports=container_ports if len(container_ports) > 0 else None, | |
restart_policy=self.container_restart_policy, | |
stdin_open=self.container_stdin_open, | |
stderr=self.container_stderr, | |
stdout=self.container_stdout, | |
tty=self.container_tty, | |
user=self.container_user, | |
volumes=container_volumes if len(container_volumes) > 0 else None, | |
working_dir=self.container_working_dir, | |
use_cache=self.use_cache, | |
) | |
# -*- List of DockerResources created by this App | |
app_resources: List[DockerResource] = [] | |
if self.image: | |
app_resources.append(self.image) | |
app_resources.extend( | |
[ | |
DockerNetwork(name=build_context.network), | |
docker_container, | |
] | |
) | |
logger.debug(f"------------ {self.get_app_name()} Built ------------") | |
return app_resources | |