AmmarFahmy
adding all files
105b369
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