Spaces:
Runtime error
Runtime error
File size: 16,972 Bytes
105b369 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 |
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
|