Source code for deploy_tools.app_builder

import hashlib
import uuid
from itertools import chain
from pathlib import Path
from urllib.request import urlretrieve

from deploy_tools.models.binary_app import BinaryApp, HashType

from .apptainer import create_sif_file
from .errors import DeployToolsError
from .layout import ModuleBuildLayout
from .models.apptainer_app import ApptainerApp
from .models.module import Application, Module
from .models.shell_app import ShellApp
from .templater import Templater, TemplateType

ALL_READ_EXECUTE_PERMISSIONS = 0o555


[docs] class AppBuilderError(DeployToolsError): """Raised when building an application's files fails."""
[docs] class AppBuilder: """Class for creating application entrypoints and associated files.""" def __init__(self, templater: Templater, build_layout: ModuleBuildLayout) -> None: self._templater = templater self._build_layout = build_layout
[docs] def create_application_files(self, app: Application, module: Module) -> None: """Create the entrypoint and supporting files for an application. The files produced depend on the application type (Apptainer, shell or binary). """ match app: case ApptainerApp(): self._create_apptainer_files(app, module) case ShellApp(): self._create_shell_file(app, module) case BinaryApp(): self._create_binary_file(app, module)
def _create_apptainer_files(self, app: ApptainerApp, module: Module) -> None: """Create apptainer entrypoints using a specified image and commands.""" self._generate_sif_file(app, module) entrypoints_folder = self._build_layout.get_entrypoints_folder( module.name, module.version ) relative_sif_file = self._get_sif_file_path(app, module).relative_to( entrypoints_folder, walk_up=True ) global_options = app.global_options for entrypoint in app.entrypoints: options = entrypoint.options entrypoint_file = entrypoints_folder / entrypoint.name mounts = ",".join(chain(global_options.mounts, options.mounts)).strip() host_binaries = " ".join( chain(global_options.host_binaries, options.host_binaries) ).strip() apptainer_args = f"{global_options.apptainer_args} {options.apptainer_args}" apptainer_args = apptainer_args.strip() command = entrypoint.command if entrypoint.command else entrypoint.name command_args = f"{global_options.command_args} {options.command_args}" command_args = command_args.strip() params = { "mounts": mounts, "host_binaries": host_binaries, "apptainer_args": apptainer_args, "relative_sif_file": relative_sif_file, "command": command, "command_args": command_args, } self._templater.create( entrypoint_file, TemplateType.APPTAINER_ENTRYPOINT, params, executable=True, create_parents=True, ) def _generate_sif_file(self, app: ApptainerApp, module: Module) -> None: sif_file_path = self._get_sif_file_path(app, module) create_sif_file(sif_file_path, app.container.url, create_parents=True) def _get_sif_file_path(self, app: ApptainerApp, module: Module) -> Path: sif_folder = self._build_layout.get_sif_files_folder( module.name, module.version ) file_name = uuid.uuid3(uuid.NAMESPACE_URL, app.container.url).hex return sif_folder / f"{file_name}.sif" def _create_shell_file(self, app: ShellApp, module: Module) -> None: """Create shell script using Bash for improved functionality.""" entrypoint_file = ( self._build_layout.get_entrypoints_folder(module.name, module.version) / app.name ) parameters = {"script": app.script} self._templater.create( entrypoint_file, TemplateType.SHELL_ENTRYPOINT, parameters, executable=True, create_parents=True, ) def _create_binary_file(self, app: BinaryApp, module: Module) -> None: """Create executable binary from the given URL. This will download the binary from the provided URL, validate it against its hash, make it executable and add it to the PATH. """ binary_folder = self._build_layout.get_entrypoints_folder( module.name, module.version ) binary_path = binary_folder / app.name binary_path.parent.mkdir(parents=True, exist_ok=True) urlretrieve(str(app.url), binary_path) match app.hash_type: case HashType.SHA256: h = hashlib.sha256() case HashType.SHA512: h = hashlib.sha512() case HashType.MD5: h = hashlib.md5() case HashType.NONE: h = None if h is not None: with open(binary_path, "rb") as fh: while True: data = fh.read(4096) if len(data) == 0: break h.update(data) if h.hexdigest() != app.hash: raise AppBuilderError(f"Downloaded Binary {app.url} hash check failed") binary_path.chmod(ALL_READ_EXECUTE_PERMISSIONS)