Skip to content

fix: Podman compatibility and testing #142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 45 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
79bd559
Handle empty strings and repo urls in assertions
angela-ko Apr 29, 2025
4ab50b3
Handle more strings
heitorPB Apr 30, 2025
6e0eb83
refactor logic to get an image
heitorPB Apr 30, 2025
265ed01
Handle image_name with URL
heitorPB Apr 30, 2025
cbf1484
Change to any
angela-ko Apr 30, 2025
e43a573
Change image exists
angela-ko Apr 30, 2025
eec02a7
test: skip tesseract_serve_ports with range on Podman
heitorPB Apr 30, 2025
18adba4
ci: run all E2E tests with Podman
heitorPB Apr 30, 2025
ee16afe
Add proper substring matching for image names
angela-ko Apr 30, 2025
04fa690
Podman typeguard
angela-ko Apr 30, 2025
479d9e1
ci: display status of podman socket for debugging
heitorPB May 2, 2025
507f6ec
Docker-py does not support partial string matching
heitorPB Apr 30, 2025
5648db6
ci(tests): test podman setup
heitorPB May 5, 2025
6136e43
Runs on multiple Ubuntus
heitorPB May 5, 2025
4bd8249
ci(test-podman): fix command to run `tesseract`
heitorPB May 5, 2025
73910ba
Fix the fixed command to build the images
heitorPB May 5, 2025
fc03af6
ci(tests): use Ubuntu24 for Podman tests
heitorPB May 5, 2025
a9eabc3
ci(tests): refactor to use a matrix for Podman
heitorPB May 6, 2025
3d4024d
use curly braces?
heitorPB May 6, 2025
b497733
Add spaces?
heitorPB May 6, 2025
dc0496c
- -> _
heitorPB May 6, 2025
dd43c07
Merge branch 'main' into ako/podman_url
heitorPB May 6, 2025
605d82e
No more braces; use single quotes
heitorPB May 6, 2025
28697f1
Try running Podman tests on all tessies
heitorPB May 6, 2025
d002512
Test all unit tessies with Podman
heitorPB May 6, 2025
f5d4000
Run base E2E + vectoradd_jax tests with Podman
heitorPB May 6, 2025
5564ef5
Add ubuntu 24 to runs on
angela-ko May 6, 2025
a449c25
Clear up some disk space
heitorPB May 6, 2025
d899c23
ci(tests): replace docker_engine -> docker-engine
heitorPB May 7, 2025
3fdd90e
ci(tests): use Ubuntu 24.04 to run E2E tests
heitorPB May 7, 2025
365cba6
refactor(sdk): better type hint for DockerException
heitorPB May 7, 2025
451e705
Make method private
heitorPB May 7, 2025
c822fba
Readd check for image name in image tags
heitorPB May 7, 2025
a6cf5f4
Revert "Readd check for image name in image tags"
heitorPB May 7, 2025
3ef4bed
Add docker_cleanup
angela-ko May 8, 2025
e754a17
remove unnecessary tag check
angela-ko May 8, 2025
5e153d3
convert port ranges to ports before passing to docker-compose
dionhaefner May 8, 2025
1bff396
Merge branch 'main' into ako/podman_url
dionhaefner May 8, 2025
597282d
fix tests
dionhaefner May 8, 2025
41bc3cf
try w/o deletion
dionhaefner May 8, 2025
e8f559d
Merge branch 'ako/podman_url' of github.com:pasteurlabs/tesseract-cor…
dionhaefner May 8, 2025
bed1ce5
cleanup after every test by default
dionhaefner May 8, 2025
9f2b81c
use cpu torch
dionhaefner May 8, 2025
fe96ccb
Address comments
angela-ko May 8, 2025
fa805e1
Merge branch 'main' into ako/podman_url
dionhaefner May 8, 2025
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
145 changes: 144 additions & 1 deletion .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ jobs:
subjobs=$(echo "$subjobs" | jq -c -r '. + ["base"]')
printf 'matrix=%s' "$subjobs" >> $GITHUB_OUTPUT

tests-e2e:
tests-e2e-docker:
needs: get-e2e-matrix

strategy:
Expand Down Expand Up @@ -236,3 +236,146 @@ jobs:
slug: pasteurlabs/tesseract-core
files: coverage*.xml
fail_ci_if_error: true

tests-e2e-podman:
needs: get-e2e-matrix

strategy:
matrix:
os: [ubuntu-24.04]
python-version: ["3.12"]

arch: ["x64"]

unit-tesseract: ${{ fromJson(needs.get-e2e-matrix.outputs.matrix) }}

include:
# Test on arm to ensure compatibility with Apple M1 chips
# (OSX runners don't have access to Docker so we use Linux ARM runners instead)
- os: "ubuntu-22.04"
python-version: "3.12"
arch: "arm"
unit-tesseract: "base"

fail-fast: false

runs-on: ${{ matrix.arch == 'x64' && matrix.os == 'ubuntu-24.04' && 'ubuntu-24.04' || matrix.arch == 'arm' && matrix.os == 'ubuntu-24.04' && 'linux-arm64-ubuntu2204' }}

defaults:
run:
shell: bash -el {0}

steps:
- name: Set up Git repository
uses: actions/checkout@v4

# Use Conda to install Python (setup-python action doesn't support ARM)
- uses: conda-incubator/setup-miniconda@v3
with:
auto-update-conda: true
python-version: ${{ matrix.python-version }}

- name: Set up uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Restore UV environment
run: cp production.uv.lock uv.lock

- name: Install package
run: |
uv sync --extra dev --frozen

- name: Set up Podman
run: |
systemctl start --user podman.socket
systemctl status --user podman.socket

podman -v
cat /etc/os-release

- name: Run test suite
run: |
if [ "${{ matrix.unit-tesseract }}" == "base" ]; then
DOCKER_HOST=unix:///run/user/1001/podman/podman.sock \
uv run --no-sync pytest \
--always-run-endtoend \
--cov-report=term-missing:skip-covered \
--cov-report=xml:coverage.xml \
--cov=tesseract_core \
tests/endtoend_tests \
-k "not test_examples"
else
DOCKER_HOST=unix:///run/user/1001/podman/podman.sock \
uv run --no-sync pytest \
--always-run-endtoend \
--cov-report=term-missing:skip-covered \
--cov-report=xml:coverage.xml \
--cov=tesseract_core \
tests/endtoend_tests/test_examples.py \
-k "[${{ matrix.unit-tesseract }}]"
fi

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5.4.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: pasteurlabs/tesseract-core
files: coverage*.xml
fail_ci_if_error: true


test-podman:
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-24.04]
runs-on: ${{ matrix.os }}

steps:
- name: Set up Git repository
uses: actions/checkout@v4

# Use Conda to install Python (setup-python action doesn't support ARM)
- uses: conda-incubator/setup-miniconda@v3
with:
auto-update-conda: true
python-version: ${{ matrix.python-version }}

- name: Set up uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Restore UV environment
run: cp production.uv.lock uv.lock

- name: Install package
run: |
uv sync --extra dev --frozen

- name: Set up Podman
run: |
systemctl start --user podman.socket
systemctl status --user podman.socket

podman -v
cat /etc/os-release

- name: Build vectoradd via `podman`
run: |
path=$(uv run tesseract build --generate-only examples/vectoradd)

podman buildx build --load --tag vectoradd --file "${path}/Dockerfile" "${path}"

podman image ls

- name: Build vectoradd via `docker`
run: |
path=$(uv run tesseract build --generate-only examples/vectoradd)

DOCKER_HOST=unix:///run/user/1001/podman/podman.sock \
docker buildx build --load --tag vectoradd --file "${path}/Dockerfile" "${path}"

DOCKER_HOST=unix:///run/user/1001/podman/podman.sock \
docker image ls
46 changes: 40 additions & 6 deletions tesseract_core/sdk/docker_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,48 @@ def from_dict(cls, json_dict: dict) -> "Image":
class Images:
"""Namespace for functions to interface with Tesseract docker images."""

@staticmethod
def tag_exists(image_name: str, tags: list_) -> bool:
"""Helper function to check if image name exists in the list of tags.

Specially handling has to be done to achieve unfuzzy substring matching, i.e.
if image tag is foo/bar/image, we need to return true for foo/bar/image, bar/image,
and image, but not ar/image.

There is no equivalent in docker-py.

Params:
image_name: The image name to check.
tags: The list of tags to check against.

Returns:
True if the image name exists in the list of tags, False otherwise.
"""
if ":" not in image_name:
image_name += ":latest"

image_name_parts = image_name.strip("/").split("/")
for tag in tags:
tag_parts = tag.strip("/").split("/")
# Check if the last parts of the tag matches the subportion that image name specifies
if tag_parts[-len(image_name_parts) :] == image_name_parts:
return True
return False

@staticmethod
def get(image_id_or_name: str | bytes, tesseract_only: bool = True) -> Image:
"""Returns the metadata for a specific image.

In docker-py, there is no substring matching and the image name is the
last tag in the list of tags, so if an image has multiple tags, only
one of the tags would be able to find the image.

However, in podman, this is not the case. Podman has substring matching
by "/" segments to handle repository urls and returns images even if
partial name is specified, or if image has multiple tags.

We chose to support podman's largest string matching functionality here.

Params:
image_id_or_name: The image name or id to get.
tesseract_only: If True, only retrieves Tesseract images.
Expand Down Expand Up @@ -77,11 +115,7 @@ def is_image_id(s: str) -> bool:
image_obj.id == image_id_or_name
or image_obj.short_id == image_id_or_name
or image_id_or_name in image_obj.tags
or (
any(
tag.split("/")[-1] == image_id_or_name for tag in image_obj.tags
)
)
or Images.tag_exists(image_id_or_name, image_obj.tags)
):
return image_obj

Expand Down Expand Up @@ -711,7 +745,7 @@ class DockerException(Exception):
class BuildError(DockerException):
"""Raised when a build fails."""

def __init__(self, build_log: str) -> None:
def __init__(self, build_log: list_) -> None:
self.build_log = build_log


Expand Down
5 changes: 3 additions & 2 deletions tests/endtoend_tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def print_debug_info(result):


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

Expand Down Expand Up @@ -74,5 +74,6 @@ def build_tesseract(
assert result.exit_code == 0, result.exception

image_tags = json.loads(result.stdout.strip())
assert image_name in image_tags
assert client.images.tag_exists(image_name, image_tags)

return image_tags[0]
36 changes: 29 additions & 7 deletions tests/endtoend_tests/test_docker_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

"""End to end tests for docker cli wrapper."""

import os
import subprocess
import textwrap
from contextlib import closing
Expand Down Expand Up @@ -52,7 +53,7 @@ def test_get_image(docker_client, docker_client_built_image_name, docker_py_clie
# Get the image
image = docker_client.images.get(docker_client_built_image_name)
assert image is not None
assert docker_client_built_image_name in image.tags
assert docker_client.images.tag_exists(docker_client_built_image_name, image.tags)

docker_py_image = docker_py_client.images.get(docker_client_built_image_name)
assert docker_py_image is not None
Expand Down Expand Up @@ -100,7 +101,7 @@ def test_get_image(docker_client, docker_client_built_image_name, docker_py_clie
assert docker_py_image_list is not None
# Check that every image in image_list is also in docker_py_image_list
for image in image_list:
assert image.id in [img.id for img in docker_py_image_list]
assert image.id in [img.id for img in docker_py_image_list if img]


def test_create_image(
Expand All @@ -120,7 +121,9 @@ def test_create_image(
try:
image = docker_client.images.get(docker_client_built_image_name)
assert image is not None
assert docker_client_built_image_name in image.tags
assert docker_client.images.tag_exists(
docker_client_built_image_name, image.tags
)

image_id_obj = docker_client.images.get(image.id)
image_short_id_obj = docker_client.images.get(image.short_id)
Expand All @@ -138,7 +141,7 @@ def test_create_image(
image1 = docker_client.images.get(image1_name)
image1_name = image1_name + ":latest"
assert image1 is not None
assert image1_name in image1.tags
assert docker_client.images.tag_exists(image1_name, image1.tags)
assert image_exists(docker_client, image1_name)
assert image_exists(docker_py_client, image1_name)

Expand All @@ -155,14 +158,33 @@ def test_create_image(
# Our docker client should be able to retrieve images with just the name
image2 = docker_client.images.get(repo_url + image2_name)
image2_no_url = docker_client.images.get(image2_name)
assert docker_client.images.tag_exists(image2_name, image2.tags)

assert image2 is not None
assert image2_no_url is not None
assert image2.id == image2_py.id
assert image2_no_url.id == image2_py.id

assert image_exists(docker_client, image2_name)
assert image_exists(docker_client, repo_url + image2_name)
assert image_exists(docker_py_client, repo_url + image2_name)
# Check we are not overmatching but we are getting all possible cases
docker_host = os.environ.get("DOCKER_HOST", "")

podman = False
if "podman" in docker_host:
podman = True

for client in [docker_client, docker_py_client]:
assert not image_exists(client, "create_image")

if podman and client == docker_py_client:
# Docker-py does not support partial string matching
assert image_exists(client, image2_name)
assert image_exists(client, f"/{image2_name}")
assert image_exists(client, f"bar/{image2_name}")
assert image_exists(client, f"bar/{image2_name}:latest")
assert not image_exists(client, f"ar/{image2_name}")
assert image_exists(client, f"foo/bar/{image2_name}")

assert image_exists(client, repo_url + image2_name)

finally:
# Clean up the images
Expand Down
19 changes: 17 additions & 2 deletions tests/endtoend_tests/test_endtoend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""End-to-end tests for Tesseract workflows."""

import json
import os
from pathlib import Path

import pytest
Expand All @@ -18,7 +19,9 @@
@pytest.fixture(scope="module")
def built_image_name(docker_client, shared_dummy_image_name, dummy_tesseract_location):
"""Build the dummy Tesseract image for the tests."""
image_name = build_tesseract(dummy_tesseract_location, shared_dummy_image_name)
image_name = build_tesseract(
docker_client, dummy_tesseract_location, shared_dummy_image_name
)
assert image_exists(docker_client, image_name)
yield image_name

Expand Down Expand Up @@ -56,7 +59,11 @@ def test_build_from_init_endtoend(
config_override["build_config.base_image"] = base_image

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

Expand Down Expand Up @@ -325,6 +332,14 @@ def test_tesseract_serve_ports(built_image_name, port):
cli_runner = CliRunner(mix_stderr=False)
project_id = None

docker_host = os.environ.get("DOCKER_HOST", "")

if "-" in port and "podman" in docker_host:
pytest.skip(
"Podman does not support port ranges in compose."
"See https://github.com/containers/podman/issues/15111"
)

# Serve tesseract on specified ports.
run_res = cli_runner.invoke(
app,
Expand Down
Loading
Loading