Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/creating-tesseracts/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ various circumstances:
There are several steps in the process of building a Tesseract image
which can be configured via the `tesseract_config.yaml` file, in particular the `build_config` section.
For example:
- By default the base image is `python:3.12-slim-bookworm`.
- By default the base image is `debian:bookworm-slim`.
Depending on your specific needs (different python version,
preinstalled dependencies, ...), it might be beneficial to
specify a different one in `base_image`.
Expand Down
63 changes: 63 additions & 0 deletions examples/cuda/tesseract_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright 2025 Pasteur Labs. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

# Tesseract API module for cuda-image
# Generated by tesseract 0.8.2.dev14+g494ed54.d20250401 on 2025-04-04T18:55:51.909676


from pydantic import BaseModel

#
# Schemas
#


class InputSchema(BaseModel):
pass


class OutputSchema(BaseModel):
pass


#
# Required endpoints
#


def apply(inputs: InputSchema) -> OutputSchema:
"""Assert that CUDA is available."""
from ctypes.util import find_library

cudart = find_library("cudart")
assert cudart is not None, "CUDA runtime library not found"
return OutputSchema()


#
# Optional endpoints
#

# import numpy as np

# def jacobian(inputs: InputSchema, jac_inputs: set[str], jac_outputs: set[str]):
# return {}

# def jacobian_vector_product(
# inputs: InputSchema,
# jvp_inputs: set[str],
# jvp_outputs: set[str],
# tangent_vector: dict[str, np.typing.ArrayLike]
# ) -> dict[str, np.typing.ArrayLike]:
# return {}

# def vector_jacobian_product(
# inputs: InputSchema,
# vjp_inputs: set[str],
# vjp_outputs: set[str],
# cotangent_vector: dict[str, np.typing.ArrayLike]
# ) -> dict[str, np.typing.ArrayLike]:
# return {}

# def abstract_eval(abstract_inputs):
# return {}
27 changes: 27 additions & 0 deletions examples/cuda/tesseract_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Tesseract configuration file
# Generated by tesseract 0.8.2.dev14+g494ed54.d20250401 on 2025-04-04T18:55:51.909676

name: "cuda-image"
version: "0+unknown"
description: ""

build_config:
# Base image to use for the container, must be Ubuntu or Debian-based
base_image: "nvidia/cuda:12.8.1-runtime-ubuntu24.04"

# Platform to build the container for. In general, images can only be executed
# on the platform they were built for.
target_platform: "native"

# Additional packages to install in the container (via apt-get)
# extra_packages:
# - package_name

# Data to copy into the container, relative to the project root
# package_data:
# - [path/to/source, path/to/destination]

# Additional Dockerfile commands to run during the build process
# custom_build_steps:
# - |
# RUN echo "Hello, World!"
9 changes: 9 additions & 0 deletions examples/cuda/tesseract_requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Tesseract requirements file
# Generated by tesseract 0.8.2.dev14+g494ed54.d20250401 on 2025-04-04T18:55:51.909676

# Add Python requirements like this:
# numpy==1.18.1

# This may contain private dependencies via SSH URLs:
# git+ssh://[email protected]/username/repo.git@branch
# (use `tesseract build --forward-ssh-agent` to grant the builder access to your SSH keys)
3 changes: 1 addition & 2 deletions examples/helloworld/tesseract_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ description: "A sample Python app"

build_config:
# Base image to use for the container, must be Ubuntu or Debian-based
# with Python 3.9 or later
# base_image: "python:3.12-slim-bookworm"
# base_image: "debian:bookworm-slim"

# Platform to build the container for. In general, images can only be executed
# on the platform they were built for.
Expand Down
2 changes: 1 addition & 1 deletion examples/meshstats/tesseract_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ name: "meshstats"
version: "0.0.1"

build_config:
base_image: "python:3.12-slim-bookworm"
base_image: "debian:bookworm-slim"
2 changes: 1 addition & 1 deletion tesseract_core/sdk/api_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class TesseractBuildConfig(BaseModel):
"""Configuration options for building a Tesseract."""

base_image: StrictStr = Field(
"python:3.12-slim-bookworm",
"debian:bookworm-slim",
description="Base Docker image for the build. Must be Debian-based.",
)
target_platform: StrictStr = Field(
Expand Down
5 changes: 3 additions & 2 deletions tesseract_core/sdk/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ def prepare_build_context(
# When building from a requirements.txt we support local dependencies.
# We separate local dep. lines from the requirements.txt and copy the
# corresponding files into the build directory.
local_requirements_path = context_dir / "local_requirements"
Path.mkdir(local_requirements_path, parents=True, exist_ok=True)

if requirement_config.provider == "python-pip":
reqstxt = src_dir / requirement_config._filename
if reqstxt.exists():
Expand All @@ -325,8 +328,6 @@ def prepare_build_context(
local_dependencies, remote_dependencies = [], []

if local_dependencies:
local_requirements_path = context_dir / "local_requirements"
Path.mkdir(local_requirements_path)
for dependency in local_dependencies:
src = src_dir / dependency
dest = context_dir / "local_requirements" / src.name
Expand Down
30 changes: 22 additions & 8 deletions tesseract_core/sdk/templates/Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ FROM {{ config.build_config.base_image }} AS build_stage
FROM --platform={{ config.build_config.target_platform }} {{ config.build_config.base_image }} AS build_stage
{% endif %}

# Install Python if necessary
RUN if [ ! -x "$(command -v python3)" ]; then \
apt-get update && apt-get install -y --no-install-recommends \
python3-dev \
python3-pip \
python3-venv \
&& rm -rf /var/lib/apt/lists/*; \
fi

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
git \
ssh \
git \
ssh \
&& rm -rf /var/lib/apt/lists/*

{% if config.build_config.extra_packages %}
Expand All @@ -26,9 +34,9 @@ RUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
# Copy all dependencies, for both the specific Tesseract and the runtime
WORKDIR /tmp/build
COPY {{ tesseract_runtime_location }} ./tesseract_runtime/
COPY {{ tesseract_source_directory }}/{{ config.build_config.requirements._filename }}* ./
COPY {{ tesseract_source_directory }}/{{ config.build_config.requirements._filename }} ./
COPY {{ tesseract_source_directory }}/{{ config.build_config.requirements._build_script }} ./
COPY local_requirements* ./local_requirements
COPY local_requirements/ ./local_requirements

# Build a python venv from python provider build scripts.
# The build script has to create a venv at /python-env
Expand All @@ -42,6 +50,12 @@ FROM {{ config.build_config.base_image }} AS run_stage
FROM --platform={{ config.build_config.target_platform }} {{ config.build_config.base_image }} AS run_stage
{% endif %}

RUN if [ ! -x "$(command -v python3)" ]; then \
apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*; \
fi

{% if config.build_config.extra_packages %}
# Install extra packages again since they may include runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
Expand All @@ -50,8 +64,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
{% endif %}

# Drop to a non-root user
RUN groupadd -g 1000 tesseractor && \
useradd -u 1000 -g 1000 --create-home -s /bin/bash tesseractor
RUN groupadd -o -g 1000 tesseractor && \
useradd -o -u 1000 -g 1000 --create-home -s /bin/bash tesseractor
WORKDIR /tesseract
RUN chown tesseractor:tesseractor /tesseract
USER tesseractor
Expand All @@ -64,7 +78,7 @@ ENV TESSERACT_NAME="{{ config.name }}" \

# Copy only necessary files
COPY --from=build_stage /python-env /python-env
COPY "{{ tesseract_source_directory }}/tesseract_api.py" ${TESSERACT_API_PATH}
COPY --chown=1000:1000 "{{ tesseract_source_directory }}/tesseract_api.py" ${TESSERACT_API_PATH}

ENV PATH="/python-env/bin:$PATH"

Expand Down
3 changes: 1 addition & 2 deletions tesseract_core/sdk/templates/base/tesseract_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ description: ""

build_config:
# Base image to use for the container, must be Ubuntu or Debian-based
# with Python 3.9 or later
# base_image: "python:3.12-slim-bookworm"
# base_image: "debian:bookworm-slim"

# Platform to build the container for. In general, images can only be executed
# on the platform they were built for.
Expand Down
3 changes: 1 addition & 2 deletions tesseract_core/sdk/templates/jax/tesseract_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ description: ""

build_config:
# Base image to use for the container, must be Ubuntu or Debian-based
# with Python 3.9 or later
# base_image: "python:3.12-slim-bookworm"
# base_image: "debian:bookworm-slim"

# Platform to build the container for. In general, images can only be executed
# on the platform they were built for.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ description: ""

build_config:
# Base image to use for the container, must be Ubuntu or Debian-based
# with Python 3.9 or later
# base_image: "python:3.12-slim-bookworm"
# base_image: "debian:bookworm-slim"

# Platform to build the container for. In general, images can only be executed
# on the platform they were built for.
Expand Down
8 changes: 7 additions & 1 deletion tests/endtoend_tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ def print_debug_info(result):
traceback.print_exception(*result.exc_info)


def build_tesseract(sourcedir, image_name, tag=None, build_retries=3):
def build_tesseract(
sourcedir, image_name, config_override=None, tag=None, build_retries=3
):
cli_runner = CliRunner(mix_stderr=False)

build_args = [
Expand All @@ -39,6 +41,10 @@ def build_tesseract(sourcedir, image_name, tag=None, build_retries=3):
f"name={image_name}",
]

if config_override is not None:
for key, val in config_override.items():
build_args.extend(["--config-override", f"{key}={val}"])

if tag is not None:
build_args.extend(["--tag", tag])
image_name = f"{image_name}:{tag}"
Expand Down
23 changes: 19 additions & 4 deletions tests/endtoend_tests/test_endtoend.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,18 @@ def built_image_name(docker_client, shared_dummy_image_name, dummy_tesseract_loc
yield image_name


@pytest.mark.parametrize("tag", [True, False])
@pytest.mark.parametrize("recipe", [None, *AVAILABLE_RECIPES])
tested_images = ("ubuntu:24.04",)

build_matrix = [
*[(tag, None, None) for tag in (True, False)],
*[(False, r, None) for r in AVAILABLE_RECIPES],
*[(False, None, img) for img in tested_images],
]


@pytest.mark.parametrize("tag,recipe,base_image", build_matrix)
def test_build_from_init_endtoend(
docker_client, dummy_image_name, tmp_path, tag, recipe
docker_client, dummy_image_name, tmp_path, tag, recipe, base_image
):
"""Test that a trivial (empty) Tesseract image can be built from init."""
cli_runner = CliRunner(mix_stderr=False)
Expand All @@ -42,7 +50,14 @@ def test_build_from_init_endtoend(
assert yaml.safe_load(config_yaml)["name"] == dummy_image_name

img_tag = "foo" if tag else None
image_name = build_tesseract(tmp_path, dummy_image_name, tag=img_tag)

config_override = {}
if base_image is not None:
config_override["build_config.base_image"] = base_image

image_name = build_tesseract(
tmp_path, dummy_image_name, config_override=config_override, tag=img_tag
)
assert image_exists(docker_client, image_name)

# Test that the image can be run and that --help is forwarded correctly
Expand Down
3 changes: 3 additions & 0 deletions tests/endtoend_tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,9 @@ class Config:
"package_data": Config(
test_with_random_inputs=True,
),
"cuda": Config(
test_with_random_inputs=True,
),
"meshstats": Config(
test_with_random_inputs=False,
sample_requests=[
Expand Down
Loading