Skip to content

Commit

Permalink
Merge pull request #9 from MITLibraries/TIMX-340-build-ab-images
Browse files Browse the repository at this point in the history
TIMX-340-build-ab-images
  • Loading branch information
ehanson8 authored Sep 6, 2024
2 parents b6f0c24 + 012be9c commit 5a8b6ec
Show file tree
Hide file tree
Showing 6 changed files with 520 additions and 38 deletions.
8 changes: 6 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ name = "pypi"

[packages]
click = "*"
docker = "*"
pygit2 = "*"
types-docker = "*"
types-pygit2 = "*"

[dev-packages]
black = "*"
coveralls = "*"
ipython = "*"
mypy = "*"
pre-commit = "*"
pytest = "*"
ruff = "*"
ipython = "*"
pytest-freezegun = "*"
ruff = "*"
setuptools = "*"

[requires]
Expand Down
362 changes: 329 additions & 33 deletions Pipfile.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion abdiff/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
All primary functions used by CLI are importable from here.
"""

from abdiff.core.build_ab_images import build_ab_images
from abdiff.core.init_job import init_job
from abdiff.core.init_run import init_run

__all__ = ["init_job", "init_run"]
__all__ = ["init_job", "init_run", "build_ab_images"]
98 changes: 97 additions & 1 deletion abdiff/core/build_ab_images.py
Original file line number Diff line number Diff line change
@@ -1 +1,97 @@
"""abdiff.core.build_ab_images"""
import logging
import tempfile

import docker
import docker.models
import docker.models.images
from pygit2 import clone_repository
from pygit2.enums import ResetMode

from abdiff.core.utils import update_or_create_job_json

logger = logging.getLogger(__name__)


def build_ab_images(
job_directory: str,
commit_sha_a: str,
commit_sha_b: str,
docker_client: docker.client.DockerClient | None = None,
) -> tuple[str, str]:
"""Build Docker images based on 2 commit SHAs.
Args:
job_directory: The directory containing all files related to a job.
commit_sha_a: The SHA of the first commit for comparison.
commit_sha_b: The SHA of the second commit for comparison.
docker_client: A configured Docker client.
"""
if not docker_client:
docker_client = docker.from_env()

image_tags = []
for commit_sha in [commit_sha_a, commit_sha_b]:
logger.debug(f"Processing commit: {commit_sha}")
image_tag = f"transmogrifier-{job_directory.split("/")[-1]}-{commit_sha}:latest"
if docker_image_exists(docker_client, image_tag):
logger.debug(f"Docker image already exists with tag: {image_tag}")
image_tags.append(image_tag)
else:
image = build_image(job_directory, commit_sha, docker_client)
image_tags.append(image.tags[0])
logger.debug(f"Finished processing commit: {commit_sha}")

images_data = {"image_tag_a": image_tags[0], "image_tag_b": image_tags[1]}
update_or_create_job_json(job_directory, images_data)
return (image_tags[0], image_tags[1])


def docker_image_exists(
docker_client: docker.client.DockerClient, image_tag: str
) -> bool:
"""Check if Docker image already exists with a certain name.
Args:
docker_client: A configured Docker client.
image_tag: The tag of the Docker image to be created.
"""
return image_tag in [
image_tag for image in docker_client.images.list() for image_tag in image.tags
]


def build_image(
job_directory: str,
commit_sha: str,
docker_client: docker.client.DockerClient,
) -> docker.models.images.Image:
"""Clone repo and build Docker image.
Args:
job_directory: The directory containing all files related to a job.
commit_sha: The SHA of the commit.
docker_client: A configured Docker client.
"""
with tempfile.TemporaryDirectory() as clone_directory:
image_tag = f"transmogrifier-{job_directory.split("/")[-1]}-{commit_sha}"
clone_repo_and_reset_to_commit(clone_directory, commit_sha)
image, _ = docker_client.images.build(path=clone_directory, tag=image_tag)
logger.debug(f"Docker image created with tag: {image}")
return image


def clone_repo_and_reset_to_commit(clone_directory: str, commit_sha: str) -> None:
"""Clone GitHub repo and reset to a specified commit.
Args:
clone_directory: The directory for the cloned repo.
commit_sha: The SHA of a repo commit.
"""
logger.debug(f"Cloning repo to: {clone_directory}")
repository = clone_repository(
"https://github.com/MITLibraries/transmogrifier.git",
clone_directory,
)
logger.debug(f"Cloned repo to: {clone_directory}")
repository.reset(commit_sha, ResetMode.HARD)
logger.debug(f"Cloned repo reset to commit: {commit_sha}")
19 changes: 18 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import MagicMock

import pytest
from click.testing import CliRunner

Expand All @@ -7,7 +9,6 @@
@pytest.fixture(autouse=True)
def _test_env(monkeypatch, tmp_path):
monkeypatch.setenv("WORKSPACE", "test")
monkeypatch.setenv("ROOT_WORKING_DIRECTORY", str(tmp_path / "output"))


@pytest.fixture
Expand All @@ -28,3 +29,19 @@ def example_job_directory():
@pytest.fixture
def job(job_directory):
return init_job(job_directory)


@pytest.fixture
def mocked_docker_client():
docker_client = MagicMock()
docker_images = []
for image_tag in [
"transmogrifier-example-job-1-abc123:latest",
"transmogrifier-example-job-1-def456:latest",
]:
docker_image = MagicMock()
docker_image.tags = [image_tag]
docker_images.append((docker_image, ""))
docker_client.images.build.side_effect = docker_images
docker_client.images.list.return_value = [docker_image]
return docker_client
68 changes: 68 additions & 0 deletions tests/test_build_ab_images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import os
from unittest.mock import patch

from abdiff.core.build_ab_images import (
build_ab_images,
build_image,
clone_repo_and_reset_to_commit,
docker_image_exists,
)
from abdiff.core.utils import read_job_json


@patch("abdiff.core.build_ab_images.clone_repository")
def test_build_ab_images_success(mocked_clone, job_directory, mocked_docker_client):
side_effect = os.makedirs(job_directory + "/clone")
mocked_clone.side_effect = side_effect

images = build_ab_images(
job_directory,
"abc123",
"def456",
mocked_docker_client,
)
assert images[0] == "transmogrifier-example-job-1-abc123:latest"
assert images[1] == "transmogrifier-example-job-1-def456:latest"
assert read_job_json(job_directory) == {
"image_tag_a": "transmogrifier-example-job-1-abc123:latest",
"image_tag_b": "transmogrifier-example-job-1-def456:latest",
}


def test_docker_image_exists_returns_true(mocked_docker_client):
assert (
docker_image_exists(
mocked_docker_client, "transmogrifier-example-job-1-def456:latest"
)
is True
)


def test_docker_image_exists_returns_false(mocked_docker_client):
assert (
docker_image_exists(
mocked_docker_client, "transmogrifier-example-job-1-abc123:latest"
)
is False
)


@patch("abdiff.core.build_ab_images.clone_repository")
def test_build_image_success(mocked_clone, job_directory, mocked_docker_client):
image = build_image(
job_directory,
"abc123",
mocked_docker_client,
)
assert image.tags[0] == "transmogrifier-example-job-1-abc123:latest"


@patch("abdiff.core.build_ab_images.clone_repository")
def test_clone_repo_and_reset_to_commit_success(mocked_clone, job_directory):
clone_directory = job_directory + "/clone"
assert not os.path.exists(clone_directory)
side_effect = os.makedirs(clone_directory)
mocked_clone.side_effect = side_effect

clone_repo_and_reset_to_commit(clone_directory, "abc123")
assert os.path.exists(clone_directory)

0 comments on commit 5a8b6ec

Please sign in to comment.