import json import shlex from typing import Dict, List, Optional, Union, Literal from pathlib import Path from e2b.exceptions import BuildException, InvalidArgumentException from e2b.template.consts import STACK_TRACE_DEPTH, RESOLVE_SYMLINKS from e2b.template.dockerfile_parser import parse_dockerfile from e2b.template.readycmd import ReadyCmd, wait_for_file from e2b.template.types import ( CopyItem, Instruction, TemplateType, RegistryConfig, InstructionType, ) from e2b.template.utils import ( calculate_files_hash, get_caller_directory, make_traceback, pad_octal, read_dockerignore, read_gcp_service_account_json, get_caller_frame, validate_relative_path, ) from types import TracebackType class TemplateBuilder: """ Builder class for adding instructions to an E2B template. All methods return self to allow method chaining. """ def __init__(self, template: "TemplateBase"): self._template = template def copy( self, src: Union[Union[str, Path], List[Union[str, Path]]], dest: Union[str, Path], force_upload: Optional[Literal[True]] = None, user: Optional[str] = None, mode: Optional[int] = None, resolve_symlinks: Optional[bool] = None, gzip: Optional[bool] = None, ) -> "TemplateBuilder": """ Copy files or directories from the local filesystem into the template. :param src: Source file(s) or directory path(s) to copy :param dest: Destination path in the template :param force_upload: Force upload even if files are cached :param user: User and optionally group (user:group) to own the files :param mode: File permissions in octal format (e.g., 0o755) :param resolve_symlinks: Whether to resolve symlinks :param gzip: Whether to gzip the files before upload (default: True) :return: `TemplateBuilder` class Example ```python template.copy('requirements.txt', '/home/user/') template.copy(['app.py', 'config.py'], '/app/', mode=0o755) ``` """ srcs = [src] if isinstance(src, (str, Path)) else src # Get the caller frame for stack trace in validation errors caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1) stack_trace = make_traceback(caller_frame) for src_item in srcs: src_string = str(src_item) # Validate that the source path is a relative path within the context directory validate_relative_path(src_string, stack_trace) args = [ src_string, str(dest), user or "", pad_octal(mode) if mode else "", ] instruction: Instruction = { "type": InstructionType.COPY, "args": args, "force": force_upload or self._template._force_next_layer, "forceUpload": force_upload, "resolveSymlinks": resolve_symlinks, "gzip": gzip, } self._template._instructions.append(instruction) # Collect one stack trace per pushed instruction so build steps # stay aligned with their stack traces when copying multiple # sources self._template._collect_stack_trace() return self def copy_items(self, items: List[CopyItem]) -> "TemplateBuilder": """ Copy multiple files or directories using a list of copy items. :param items: List of CopyItem dictionaries with src, dest, and optional parameters :return: `TemplateBuilder` class Example ```python template.copy_items([ {'src': 'app.py', 'dest': '/app/'}, {'src': 'config.py', 'dest': '/app/', 'mode': 0o644} ]) ``` """ # Get the stack trace at the copy_items call site caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1) stack_trace = make_traceback(caller_frame) def _copy_items(): for item in items: try: self.copy( item["src"], item["dest"], item.get("forceUpload"), item.get("user"), item.get("mode"), item.get("resolveSymlinks"), item.get("gzip"), ) except Exception as error: # Re-raise the error with the captured stack trace if stack_trace is not None: raise error.with_traceback(stack_trace) raise # Use the override so each copied item collects this stack trace, # keeping build steps aligned with their stack traces self._template._run_in_stack_trace_override_context(_copy_items, stack_trace) return self def remove( self, path: Union[Union[str, Path], List[Union[str, Path]]], force: bool = False, recursive: bool = False, user: Optional[str] = None, ) -> "TemplateBuilder": """ Remove files or directories in the template. :param path: File(s) or directory path(s) to remove :param force: Force removal without prompting :param recursive: Remove directories recursively :param user: User to run the command as :return: `TemplateBuilder` class Example ```python template.remove('/tmp/cache', recursive=True, force=True) template.remove('/tmp/cache', recursive=True, force=True, user='root') ``` """ paths = [path] if isinstance(path, (str, Path)) else path args = ["rm"] if recursive: args.append("-r") if force: args.append("-f") args.extend([shlex.quote(str(p)) for p in paths]) return self._template._run_in_new_stack_trace_context( lambda: self.run_cmd(" ".join(args), user=user) ) def rename( self, src: Union[str, Path], dest: Union[str, Path], force: bool = False, user: Optional[str] = None, ) -> "TemplateBuilder": """ Rename or move a file or directory in the template. :param src: Source path :param dest: Destination path :param force: Force rename without prompting :param user: User to run the command as :return: `TemplateBuilder` class Example ```python template.rename('/tmp/old.txt', '/tmp/new.txt') template.rename('/tmp/old.txt', '/tmp/new.txt', user='root') ``` """ args = ["mv", shlex.quote(str(src)), shlex.quote(str(dest))] if force: args.append("-f") return self._template._run_in_new_stack_trace_context( lambda: self.run_cmd(" ".join(args), user=user) ) def make_dir( self, path: Union[Union[str, Path], List[Union[str, Path]]], mode: Optional[int] = None, user: Optional[str] = None, ) -> "TemplateBuilder": """ Create directory(ies) in the template. :param path: Directory path(s) to create :param mode: Directory permissions in octal format (e.g., 0o755) :param user: User to run the command as :return: `TemplateBuilder` class Example ```python template.make_dir('/app/data', mode=0o755) template.make_dir(['/app/logs', '/app/cache']) template.make_dir('/app/data', mode=0o755, user='root') ``` """ path_list = [path] if isinstance(path, (str, Path)) else path args = ["mkdir", "-p"] if mode: args.append(f"-m {pad_octal(mode)}") args.extend([shlex.quote(str(p)) for p in path_list]) return self._template._run_in_new_stack_trace_context( lambda: self.run_cmd(" ".join(args), user=user) ) def make_symlink( self, src: Union[str, Path], dest: Union[str, Path], user: Optional[str] = None, force: bool = False, ) -> "TemplateBuilder": """ Create a symbolic link in the template. :param src: Source path (target of the symlink) :param dest: Destination path (location of the symlink) :param user: User to run the command as :param force: Force symlink without prompting :return: `TemplateBuilder` class Example ```python template.make_symlink('/usr/bin/python3', '/usr/bin/python') template.make_symlink('/usr/bin/python3', '/usr/bin/python', user='root') template.make_symlink('/usr/bin/python3', '/usr/bin/python', force=True) ``` """ args = ["ln", "-s"] if force: args.append("-f") args.extend([shlex.quote(str(src)), shlex.quote(str(dest))]) return self._template._run_in_new_stack_trace_context( lambda: self.run_cmd(" ".join(args), user=user) ) def run_cmd( self, command: Union[str, List[str]], user: Optional[str] = None ) -> "TemplateBuilder": """ Run a shell command during template build. :param command: Command string or list of commands to run (joined with &&) :param user: User to run the command as :return: `TemplateBuilder` class Example ```python template.run_cmd('apt-get update') template.run_cmd(['pip install numpy', 'pip install pandas']) template.run_cmd('apt-get install vim', user='root') ``` """ commands = [command] if isinstance(command, str) else command args = [" && ".join(commands)] if user: args.append(user) instruction: Instruction = { "type": InstructionType.RUN, "args": args, "force": self._template._force_next_layer, "forceUpload": None, } self._template._instructions.append(instruction) self._template._collect_stack_trace() return self def set_workdir(self, workdir: Union[str, Path]) -> "TemplateBuilder": """ Set the working directory for subsequent commands in the template. :param workdir: Path to set as the working directory :return: `TemplateBuilder` class Example ```python template.set_workdir('/app') ``` """ instruction: Instruction = { "type": InstructionType.WORKDIR, "args": [str(workdir)], "force": self._template._force_next_layer, "forceUpload": None, } self._template._instructions.append(instruction) self._template._collect_stack_trace() return self def set_user(self, user: str) -> "TemplateBuilder": """ Set the user for subsequent commands in the template. :param user: Username to set :return: `TemplateBuilder` class Example ```python template.set_user('root') ``` """ instruction: Instruction = { "type": InstructionType.USER, "args": [user], "force": self._template._force_next_layer, "forceUpload": None, } self._template._instructions.append(instruction) self._template._collect_stack_trace() return self def pip_install( self, packages: Optional[Union[str, List[str]]] = None, g: bool = True ) -> "TemplateBuilder": """ Install Python packages using pip. :param packages: Package name(s) to install. If None, runs 'pip install .' in the current directory :param g: Install packages globally (default: True). If False, installs for user only :return: `TemplateBuilder` class Example ```python template.pip_install('numpy') template.pip_install(['pandas', 'scikit-learn']) template.pip_install('numpy', g=False) # Install for user only template.pip_install() # Installs from current directory ``` """ if isinstance(packages, str): packages = [packages] args = ["pip", "install"] if not g: args.append("--user") if packages: args.extend(packages) else: args.append(".") return self._template._run_in_new_stack_trace_context( lambda: self.run_cmd(" ".join(args), user="root" if g else None) ) def npm_install( self, packages: Optional[Union[str, List[str]]] = None, g: Optional[bool] = False, dev: Optional[bool] = False, ) -> "TemplateBuilder": """ Install Node.js packages using npm. :param packages: Package name(s) to install. If None, installs from package.json :param g: Install packages globally :param dev: Install packages as dev dependencies :return: `TemplateBuilder` class Example ```python template.npm_install('express') template.npm_install(['lodash', 'axios']) template.npm_install('typescript', g=True) template.npm_install() # Installs from package.json ``` """ if isinstance(packages, str): packages = [packages] args = ["npm", "install"] if g: args.append("-g") if dev: args.append("--save-dev") if packages: args.extend(packages) return self._template._run_in_new_stack_trace_context( lambda: self.run_cmd(" ".join(args), user="root" if g else None) ) def bun_install( self, packages: Optional[Union[str, List[str]]] = None, g: Optional[bool] = False, dev: Optional[bool] = False, ) -> "TemplateBuilder": """ Install Bun packages using bun. :param packages: Package name(s) to install. If None, installs from package.json :param g: Install packages globally :param dev: Install packages as dev dependencies :return: `TemplateBuilder` class Example ```python template.bun_install('express') template.bun_install(['lodash', 'axios']) template.bun_install('tsx', g=True) template.bun_install('typescript', dev=True) template.bun_install() // Installs from package.json ``` """ if isinstance(packages, str): packages = [packages] args = ["bun", "install"] if g: args.append("-g") if dev: args.append("--dev") if packages: args.extend(packages) return self._template._run_in_new_stack_trace_context( lambda: self.run_cmd(" ".join(args), user="root" if g else None) ) def apt_install( self, packages: Union[str, List[str]], no_install_recommends: bool = False, fix_missing: bool = False, ) -> "TemplateBuilder": """ Install system packages using apt-get. :param packages: Package name(s) to install :param no_install_recommends: Whether to install recommended packages :param fix_missing: Whether to fix missing packages :return: `TemplateBuilder` class Example ```python template.apt_install('vim') template.apt_install(['git', 'curl', 'wget']) template.apt_install('vim', fix_missing=True) ``` """ if isinstance(packages, str): packages = [packages] return self._template._run_in_new_stack_trace_context( lambda: self.run_cmd( [ "apt-get update", f"DEBIAN_FRONTEND=noninteractive DEBCONF_NOWARNINGS=yes apt-get install -y {'--no-install-recommends ' if no_install_recommends else ''}{'--fix-missing ' if fix_missing else ''}{' '.join(packages)}", ], user="root", ) ) def add_mcp_server(self, servers: Union[str, List[str]]) -> "TemplateBuilder": """ Install MCP servers using mcp-gateway. Note: Requires a base image with mcp-gateway pre-installed (e.g., mcp-gateway). :param servers: MCP server name(s) :return: `TemplateBuilder` class Example ```python template.add_mcp_server('exa') template.add_mcp_server(['brave', 'firecrawl', 'duckduckgo']) ``` """ if self._template._base_template != "mcp-gateway": caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1) stack_trace = make_traceback(caller_frame) raise BuildException( "MCP servers can only be added to mcp-gateway template" ).with_traceback(stack_trace) if isinstance(servers, str): servers = [servers] return self._template._run_in_new_stack_trace_context( lambda: self.run_cmd(f"mcp-gateway pull {' '.join(servers)}", user="root") ) def git_clone( self, url: str, path: Optional[Union[str, Path]] = None, branch: Optional[str] = None, depth: Optional[int] = None, user: Optional[str] = None, ) -> "TemplateBuilder": """ Clone a git repository into the template. :param url: Git repository URL :param path: Destination path for the clone :param branch: Branch to clone :param depth: Clone depth for shallow clones :param user: User to run the command as :return: `TemplateBuilder` class Example ```python template.git_clone('https://github.com/user/repo.git', '/app/repo') template.git_clone('https://github.com/user/repo.git', branch='main', depth=1) template.git_clone('https://github.com/user/repo.git', '/app/repo', user='root') ``` """ args = ["git", "clone", shlex.quote(url)] if branch: args.append(f"--branch {shlex.quote(branch)}") args.append("--single-branch") if depth: args.append(f"--depth {depth}") if path: args.append(shlex.quote(str(path))) return self._template._run_in_new_stack_trace_context( lambda: self.run_cmd(" ".join(args), user=user) ) def beta_dev_container_prebuild( self, devcontainer_directory: Union[str, Path], ) -> "TemplateBuilder": """ Prebuild a devcontainer from the specified directory during the build process. :param devcontainer_directory: Path to the devcontainer directory :return: `TemplateBuilder` class Example ```python template.git_clone('https://myrepo.com/project.git', '/my-devcontainer') template.beta_dev_container_prebuild('/my-devcontainer') ``` """ if self._template._base_template != "devcontainer": caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1) stack_trace = make_traceback(caller_frame) raise BuildException( "Devcontainers can only used in the devcontainer template" ).with_traceback(stack_trace) return self._template._run_in_new_stack_trace_context( lambda: self.run_cmd( f"devcontainer build --workspace-folder {shlex.quote(str(devcontainer_directory))}", user="root", ) ) def beta_set_dev_container_start( self, devcontainer_directory: Union[str, Path], ) -> "TemplateFinal": """ Start a devcontainer from the specified directory and set it as the start command. This method returns `TemplateFinal`, which means it must be the last method in the chain. :param devcontainer_directory: Path to the devcontainer directory :return: `TemplateFinal` class Example ```python # Simple start template.git_clone('https://myrepo.com/project.git', '/my-devcontainer') template.beta_set_devcontainer_start('/my-devcontainer') # With prebuild template.git_clone('https://myrepo.com/project.git', '/my-devcontainer') template.beta_dev_container_prebuild('/my-devcontainer') template.beta_set_dev_container_start('/my-devcontainer') ``` """ if self._template._base_template != "devcontainer": caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1) stack_trace = make_traceback(caller_frame) raise BuildException( "Devcontainers can only used in the devcontainer template" ).with_traceback(stack_trace) def _set_start(): dir_ = shlex.quote(str(devcontainer_directory)) return self.set_start_cmd( "sudo devcontainer up --workspace-folder " + dir_ + " && sudo /prepare-exec.sh " + dir_ + " | sudo tee /devcontainer.sh > /dev/null && sudo chmod +x /devcontainer.sh && sudo touch /devcontainer.up", wait_for_file("/devcontainer.up"), ) return self._template._run_in_new_stack_trace_context(_set_start) def set_envs(self, envs: Dict[str, str]) -> "TemplateBuilder": """ Set environment variables. Note: Environment variables defined here are available only during template build. :param envs: Dictionary of environment variable names and values :return: `TemplateBuilder` class Example ```python template.set_envs({'NODE_ENV': 'production', 'PORT': '8080'}) ``` """ if len(envs) == 0: return self instruction: Instruction = { "type": InstructionType.ENV, "args": [item for key, value in envs.items() for item in [key, value]], "force": self._template._force_next_layer, "forceUpload": None, } self._template._instructions.append(instruction) self._template._collect_stack_trace() return self def skip_cache(self) -> "TemplateBuilder": """ Skip cache for all subsequent build instructions from this point. Call this before any instruction to force it and all following layers to be rebuilt, ignoring any cached layers. :return: `TemplateBuilder` class Example ```python template.skip_cache().run_cmd('apt-get update') ``` """ self._template._force_next_layer = True return self def set_start_cmd( self, start_cmd: str, ready_cmd: Union[str, ReadyCmd] ) -> "TemplateFinal": """ Set the command to start when the sandbox launches and the ready check command. :param start_cmd: Command to run when the sandbox starts :param ready_cmd: Command or ReadyCmd to check if the sandbox is ready :return: `TemplateFinal` class Example ```python # Using a string command template.set_start_cmd( 'python app.py', 'curl http://localhost:8000/health' ) # Using ReadyCmd helpers from e2b import wait_for_port, wait_for_url template.set_start_cmd( 'python -m http.server 8000', wait_for_port(8000) ) template.set_start_cmd( 'npm start', wait_for_url('http://localhost:3000/health', 200) ) ``` """ self._template._start_cmd = start_cmd if isinstance(ready_cmd, ReadyCmd): ready_cmd = ready_cmd.get_cmd() self._template._ready_cmd = ready_cmd self._template._collect_stack_trace() return TemplateFinal(self._template) def set_ready_cmd(self, ready_cmd: Union[str, ReadyCmd]) -> "TemplateFinal": """ Set the command to check if the sandbox is ready. :param ready_cmd: Command or ReadyCmd to check if the sandbox is ready :return: `TemplateFinal` class Example ```python # Using a string command template.set_ready_cmd('curl http://localhost:8000/health') # Using ReadyCmd helpers from e2b import wait_for_port, wait_for_file, wait_for_process template.set_ready_cmd(wait_for_port(3000)) template.set_ready_cmd(wait_for_file('/tmp/ready')) template.set_ready_cmd(wait_for_process('nginx')) ``` """ if isinstance(ready_cmd, ReadyCmd): ready_cmd = ready_cmd.get_cmd() self._template._ready_cmd = ready_cmd self._template._collect_stack_trace() return TemplateFinal(self._template) class TemplateFinal: """ Final template state after start/ready commands are set. """ def __init__(self, template: "TemplateBase"): self._template = template class TemplateBase: """ Base class for building E2B sandbox templates. """ _logs_refresh_frequency = 0.2 def __init__( self, file_context_path: Optional[Union[str, Path]] = None, file_ignore_patterns: Optional[List[str]] = None, ): """ Create a new template builder instance. :param file_context_path: Base path for resolving relative file paths in copy operations :param file_ignore_patterns: List of glob patterns to ignore when copying files """ self._default_base_image: str = "e2bdev/base" self._base_image: Optional[str] = self._default_base_image self._base_template: Optional[str] = None self._registry_config: Optional[RegistryConfig] = None self._start_cmd: Optional[str] = None self._ready_cmd: Optional[str] = None # Force the whole template to be rebuilt self._force: bool = False # Force the next layer to be rebuilt self._force_next_layer: bool = False self._instructions: List[Instruction] = [] # If no file_context_path is provided, use the caller's directory self._file_context_path = ( file_context_path.as_posix() if isinstance(file_context_path, Path) else (file_context_path or get_caller_directory(STACK_TRACE_DEPTH) or ".") ) self._file_ignore_patterns: List[str] = file_ignore_patterns or [] self._stack_traces: List[Union[TracebackType, None]] = [] self._stack_traces_enabled: bool = True self._stack_traces_override: Optional[Union[TracebackType, None]] = None def skip_cache(self) -> "TemplateBase": """ Skip cache for all subsequent build instructions from this point. :return: `TemplateBase` class Example ```python template.skip_cache().from_python_image('3.11') ``` """ self._force_next_layer = True return self def _collect_stack_trace( self, stack_traces_depth: int = STACK_TRACE_DEPTH ) -> "TemplateBase": """ Collect the current stack trace for debugging purposes. :param stack_traces_depth: Depth to traverse in the call stack :return: `TemplateBase` class """ if not self._stack_traces_enabled: return self # Use the override if set, otherwise get the caller frame if self._stack_traces_override is not None: self._stack_traces.append(self._stack_traces_override) return self stack = get_caller_frame(stack_traces_depth) self._stack_traces.append(make_traceback(stack)) return self def _disable_stack_trace(self) -> "TemplateBase": """ Temporarily disable stack trace collection. :return: `TemplateBase` class """ self._stack_traces_enabled = False return self def _enable_stack_trace(self) -> "TemplateBase": """ Re-enable stack trace collection. :return: `TemplateBase` class """ self._stack_traces_enabled = True return self def _run_in_new_stack_trace_context(self, fn): """ Execute a function in a clean stack trace context. :param fn: Function to execute :return: The result of the function """ self._disable_stack_trace() try: result = fn() finally: self._enable_stack_trace() self._collect_stack_trace(STACK_TRACE_DEPTH + 1) return result def _run_in_stack_trace_override_context( self, fn, stack_trace_override: Optional[Union[TracebackType, None]] ): """ Execute a function with a manual stack trace override. :param fn: Function to execute :param stack_trace_override: Stack trace to use instead of auto-collecting :return: The result of the function """ self._stack_traces_override = stack_trace_override try: return fn() finally: self._stack_traces_override = None # Built-in image mixins def from_debian_image(self, variant: str = "stable") -> TemplateBuilder: """ Start template from a Debian base image. :param variant: Debian image variant :return: `TemplateBuilder` class Example ```python Template().from_debian_image('bookworm') ``` """ return self._run_in_new_stack_trace_context( lambda: self.from_image(f"debian:{variant}") ) def from_ubuntu_image(self, variant: str = "latest") -> TemplateBuilder: """ Start template from an Ubuntu base image. :param variant: Ubuntu image variant (default: 'latest') :return: `TemplateBuilder` class Example ```python Template().from_ubuntu_image('24.04') ``` """ return self._run_in_new_stack_trace_context( lambda: self.from_image(f"ubuntu:{variant}") ) def from_python_image(self, version: str = "3") -> TemplateBuilder: """ Start template from a Python base image. :param version: Python version (default: '3') :return: `TemplateBuilder` class Example ```python Template().from_python_image('3') ``` """ return self._run_in_new_stack_trace_context( lambda: self.from_image(f"python:{version}") ) def from_node_image(self, variant: str = "lts") -> TemplateBuilder: """ Start template from a Node.js base image. :param variant: Node.js image variant (default: 'lts') :return: `TemplateBuilder` class Example ```python Template().from_node_image('24') ``` """ return self._run_in_new_stack_trace_context( lambda: self.from_image(f"node:{variant}") ) def from_bun_image(self, variant: str = "latest") -> TemplateBuilder: """ Start template from a Bun base image. :param variant: Bun image variant (default: 'latest') :return: `TemplateBuilder` class """ return self._run_in_new_stack_trace_context( lambda: self.from_image(f"oven/bun:{variant}") ) def from_base_image(self) -> TemplateBuilder: """ Start template from the E2B base image (e2bdev/base:latest). :return: `TemplateBuilder` class Example ```python Template().from_base_image() ``` """ return self._run_in_new_stack_trace_context( lambda: self.from_image(self._default_base_image) ) def from_image( self, image: str, username: Optional[str] = None, password: Optional[str] = None, ) -> TemplateBuilder: """ Start template from a Docker image. :param image: Docker image name (e.g., 'ubuntu:24.04') :param username: Username for private registry authentication :param password: Password for private registry authentication :return: `TemplateBuilder` class Example ```python Template().from_image('python:3') # With credentials (optional) Template().from_image('myregistry.com/myimage:latest', username='user', password='pass') ``` """ # Validate (and resolve the registry config) before mutating the builder. if username is not None or password is not None: if not username or not password: caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1) stack_trace = make_traceback(caller_frame) raise InvalidArgumentException( "Both username and password are required when providing registry credentials" ).with_traceback(stack_trace) self._registry_config = { "type": "registry", "username": username, "password": password, } self._base_image = image self._base_template = None # If we should force the next layer and it's a FROM command, invalidate whole template if self._force_next_layer: self._force = True self._collect_stack_trace() return TemplateBuilder(self) def from_template(self, template: str) -> TemplateBuilder: """ Start template from an existing E2B template. :param template: E2B template ID or alias :return: `TemplateBuilder` class Example ```python Template().from_template('my-base-template') ``` """ self._base_template = template self._base_image = None # If we should force the next layer and it's a FROM command, invalidate whole template if self._force_next_layer: self._force = True self._collect_stack_trace() return TemplateBuilder(self) def from_dockerfile(self, dockerfile_content_or_path: str) -> TemplateBuilder: """ Parse a Dockerfile and convert it to Template SDK format. :param dockerfile_content_or_path: Either the Dockerfile content as a string, or a path to a Dockerfile file :return: `TemplateBuilder` class Example ```python Template().from_dockerfile('Dockerfile') Template().from_dockerfile('FROM python:3\\nRUN pip install numpy') ``` """ # Create a TemplateBuilder first to use its methods builder = TemplateBuilder(self) # Get the caller frame to use for stack trace override # -1 as we're going up the call stack from the parse_dockerfile function caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1) stack_trace_override = make_traceback(caller_frame) # Parse the dockerfile using the builder as the interface base_image = self._run_in_stack_trace_override_context( lambda: parse_dockerfile(dockerfile_content_or_path, builder), stack_trace_override, ) self._base_image = base_image # If we should force the next layer and it's a FROM command, invalidate whole template if self._force_next_layer: self._force = True self._collect_stack_trace() return builder def from_aws_registry( self, image: str, access_key_id: str, secret_access_key: str, region: str, ) -> TemplateBuilder: """ Start template from an AWS ECR registry image. :param image: Docker image name from AWS ECR :param access_key_id: AWS access key ID :param secret_access_key: AWS secret access key :param region: AWS region :return: `TemplateBuilder` class Example ```python Template().from_aws_registry( '123456789.dkr.ecr.us-west-2.amazonaws.com/myimage:latest', access_key_id='AKIA...', secret_access_key='...', region='us-west-2' ) ``` """ self._base_image = image self._base_template = None # Set the registry config if provided self._registry_config = { "type": "aws", "awsAccessKeyId": access_key_id, "awsSecretAccessKey": secret_access_key, "awsRegion": region, } # If we should force the next layer and it's a FROM command, invalidate whole template if self._force_next_layer: self._force = True self._collect_stack_trace() return TemplateBuilder(self) def from_gcp_registry( self, image: str, service_account_json: Union[str, dict] ) -> TemplateBuilder: """ Start template from a GCP Artifact Registry or Container Registry image. :param image: Docker image name from GCP registry :param service_account_json: Service account JSON string, dict, or path to JSON file :return: `TemplateBuilder` class Example ```python Template().from_gcp_registry( 'gcr.io/myproject/myimage:latest', service_account_json='path/to/service-account.json' ) ``` """ self._base_image = image self._base_template = None # Set the registry config if provided self._registry_config = { "type": "gcp", "serviceAccountJson": read_gcp_service_account_json( self._file_context_path, service_account_json ), } # If we should force the next layer and it's a FROM command, invalidate whole template if self._force_next_layer: self._force = True self._collect_stack_trace() return TemplateBuilder(self) @staticmethod def to_json(template: "TemplateClass") -> str: """ Convert a template to JSON representation. :param template: The template to convert (TemplateBuilder or TemplateFinal instance) :return: JSON string representation of the template Example ```python template = Template().from_python_image('3').copy('app.py', '/app/') json_str = TemplateBase.to_json(template) ``` """ return json.dumps( template._template._serialize( template._template._instructions_with_hashes() ), indent=2, ) @staticmethod def to_dockerfile(template: "TemplateClass") -> str: """ Convert a template to Dockerfile format. Note: Templates based on other E2B templates cannot be converted to Dockerfile. :param template: The template to convert (TemplateBuilder or TemplateFinal instance) :return: Dockerfile string representation :raises ValueError: If the template is based on another E2B template or has no base image Example ```python template = Template().from_python_image('3').copy('app.py', '/app/') dockerfile = TemplateBase.to_dockerfile(template) ``` """ if template._template._base_template is not None: raise ValueError( "Cannot convert template built from another template to Dockerfile. " "Templates based on other templates can only be built using the E2B API." ) if template._template._base_image is None: raise ValueError("No base image specified for template") dockerfile = f"FROM {template._template._base_image}\n" for instruction in template._template._instructions: if instruction["type"] == InstructionType.RUN: dockerfile += f"RUN {instruction['args'][0]}\n" continue if instruction["type"] == InstructionType.COPY: dockerfile += ( f"COPY {instruction['args'][0]} {instruction['args'][1]}\n" ) continue if instruction["type"] == InstructionType.ENV: args = instruction["args"] values = [] for i in range(0, len(args), 2): values.append(f"{args[i]}={args[i + 1]}") dockerfile += f"ENV {' '.join(values)}\n" continue dockerfile += ( f"{instruction['type'].value} {' '.join(instruction['args'])}\n" ) if template._template._start_cmd: dockerfile += f"ENTRYPOINT {template._template._start_cmd}\n" return dockerfile def _instructions_with_hashes( self, ) -> List[Instruction]: """ Add file hashes to COPY instructions for cache invalidation. :return: Copy of instructions list with filesHash added to COPY instructions """ steps: List[Instruction] = [] for index, instruction in enumerate(self._instructions): step: Instruction = { "type": instruction["type"], "args": instruction["args"], "force": instruction["force"], "forceUpload": instruction.get("forceUpload"), "resolveSymlinks": instruction.get("resolveSymlinks"), "gzip": instruction.get("gzip"), } if instruction["type"] == InstructionType.COPY: stack_trace = None if index + 1 < len(self._stack_traces): stack_trace = self._stack_traces[index + 1] args = instruction.get("args", []) src = args[0] if len(args) > 0 else None dest = args[1] if len(args) > 1 else None if src is None or dest is None: raise ValueError("Source path and destination path are required") resolve_symlinks = instruction.get("resolveSymlinks") step["filesHash"] = calculate_files_hash( src, dest, self._file_context_path, [ *self._file_ignore_patterns, *read_dockerignore(self._file_context_path), ], resolve_symlinks if resolve_symlinks is not None else RESOLVE_SYMLINKS, stack_trace, ) steps.append(step) return steps def _serialize(self, steps: List[Instruction]) -> TemplateType: """ Serialize the template to the API request format. :param steps: List of build instructions with file hashes :return: Template data formatted for the API """ _steps: List[Instruction] = [] for _, instruction in enumerate(steps): step: Instruction = { "type": instruction.get("type"), "args": instruction.get("args"), "force": instruction.get("force"), } files_hash = instruction.get("filesHash") if files_hash is not None: step["filesHash"] = files_hash force_upload = instruction.get("forceUpload") if force_upload is not None: step["forceUpload"] = force_upload _steps.append(step) template_data: TemplateType = { "steps": _steps, "force": self._force, } if self._base_image is not None: template_data["fromImage"] = self._base_image if self._base_template is not None: template_data["fromTemplate"] = self._base_template if self._registry_config is not None: template_data["fromImageRegistry"] = self._registry_config if self._start_cmd is not None: template_data["startCmd"] = self._start_cmd if self._ready_cmd is not None: template_data["readyCmd"] = self._ready_cmd return template_data TemplateClass = Union[TemplateFinal, TemplateBuilder]