diff --git a/src/taskgraph/docker.py b/src/taskgraph/docker.py index 649bc20c3..9f849525f 100644 --- a/src/taskgraph/docker.py +++ b/src/taskgraph/docker.py @@ -18,6 +18,22 @@ from taskgraph.util import docker from taskgraph.util.taskcluster import get_artifact_url, get_session +DEPLOY_WARNING = """ +***************************************************************** +WARNING: Image is not suitable for deploying/pushing. + +To automatically tag the image the following files are required: +- {image_dir}/REGISTRY +- {image_dir}/VERSION + +The REGISTRY file contains the Docker registry hosting the image. +A default REGISTRY file may also be defined in the parent docker +directory. + +The VERSION file contains the version of the image. +***************************************************************** +""" + def get_image_digest(image_name): from taskgraph.generator import load_tasks_for_kind @@ -105,19 +121,18 @@ def build_image(name, tag, args=None): buf = BytesIO() docker.stream_context_tar(".", image_dir, buf, "", args) - subprocess.run( - ["docker", "image", "build", "--no-cache", "-t", tag, "-"], input=buf.getvalue() - ) + cmdargs = ["docker", "image", "build", "--no-cache", "-"] + if tag: + cmdargs.insert(-1, f"-t={tag}") + subprocess.run(cmdargs, input=buf.getvalue()) - print(f"Successfully built {name} and tagged with {tag}") + msg = f"Successfully built {name}" + if tag: + msg += f" and tagged with {tag}" + print(msg) - if tag.endswith(":latest"): - print("*" * 50) - print("WARNING: no VERSION file found in image directory.") - print("Image is not suitable for deploying/pushing.") - print("Create an image suitable for deploying/pushing by creating") - print("a VERSION file in the image directory.") - print("*" * 50) + if not tag or tag.endswith(":latest"): + print(DEPLOY_WARNING.format(image_dir=os.path.relpath(image_dir), image=name)) def load_image(url, imageName=None, imageTag=None): diff --git a/src/taskgraph/util/docker.py b/src/taskgraph/util/docker.py index a50561a1f..13815381e 100644 --- a/src/taskgraph/util/docker.py +++ b/src/taskgraph/util/docker.py @@ -7,6 +7,7 @@ import io import os import re +from typing import Optional from taskgraph.util.archive import create_tar_gz_from_files from taskgraph.util.memoize import memoize @@ -16,17 +17,27 @@ from .yaml import load_yaml -def docker_image(name, by_tag=False): +def docker_image(name: str, by_tag: bool = False) -> Optional[str]: """ Resolve in-tree prebuilt docker image to ``/@sha256:``, or ``/:`` if `by_tag` is `True`. + + Args: + name (str): The image to build. + by_tag (bool): If True, will apply a tag based on VERSION file. + Otherwise will apply a hash based on HASH file. + Returns: + Optional[str]: Image if it can be resolved, otherwise None. """ try: with open(os.path.join(IMAGE_DIR, name, "REGISTRY")) as f: registry = f.read().strip() except OSError: - with open(os.path.join(IMAGE_DIR, "REGISTRY")) as f: - registry = f.read().strip() + try: + with open(os.path.join(IMAGE_DIR, "REGISTRY")) as f: + registry = f.read().strip() + except OSError: + return None if not by_tag: hashfile = os.path.join(IMAGE_DIR, name, "HASH") @@ -34,7 +45,7 @@ def docker_image(name, by_tag=False): with open(hashfile) as f: return f"{registry}/{name}@{f.read().strip()}" except OSError: - raise Exception(f"Failed to read HASH file {hashfile}") + return None try: with open(os.path.join(IMAGE_DIR, name, "VERSION")) as f: diff --git a/test/data/taskcluster/docker/hello-world-tag/Dockerfile b/test/data/taskcluster/docker/hello-world-tag/Dockerfile new file mode 100644 index 000000000..5aaf0a281 --- /dev/null +++ b/test/data/taskcluster/docker/hello-world-tag/Dockerfile @@ -0,0 +1 @@ +FROM hello-world diff --git a/test/data/taskcluster/docker/hello-world-tag/REGISTRY b/test/data/taskcluster/docker/hello-world-tag/REGISTRY new file mode 100644 index 000000000..9daeafb98 --- /dev/null +++ b/test/data/taskcluster/docker/hello-world-tag/REGISTRY @@ -0,0 +1 @@ +test diff --git a/test/data/taskcluster/docker/hello-world-tag/VERSION b/test/data/taskcluster/docker/hello-world-tag/VERSION new file mode 100644 index 000000000..d3827e75a --- /dev/null +++ b/test/data/taskcluster/docker/hello-world-tag/VERSION @@ -0,0 +1 @@ +1.0 diff --git a/test/data/taskcluster/docker/hello-world/Dockerfile b/test/data/taskcluster/docker/hello-world/Dockerfile new file mode 100644 index 000000000..5aaf0a281 --- /dev/null +++ b/test/data/taskcluster/docker/hello-world/Dockerfile @@ -0,0 +1 @@ +FROM hello-world diff --git a/test/test_docker.py b/test/test_docker.py new file mode 100644 index 000000000..b4f66aad0 --- /dev/null +++ b/test/test_docker.py @@ -0,0 +1,55 @@ +import pytest + +from taskgraph import docker + + +@pytest.fixture(autouse=True, scope="module") +def mock_docker_path(module_mocker, datadir): + module_mocker.patch( + "taskgraph.util.docker.IMAGE_DIR", str(datadir / "taskcluster" / "docker") + ) + + +@pytest.fixture +def mock_docker_build(mocker): + def side_effect(topsrcdir, context_dir, out_file, image_name=None, args=None): + out_file.write(b"xyz") + + m_stream = mocker.patch.object(docker.docker, "stream_context_tar") + m_stream.side_effect = side_effect + + m_run = mocker.patch.object(docker.subprocess, "run") + return (m_stream, m_run) + + +def test_build_image(capsys, mock_docker_build): + m_stream, m_run = mock_docker_build + image = "hello-world-tag" + tag = f"test/{image}:1.0" + + assert docker.build_image(image, None) is None + m_stream.assert_called_once() + m_run.assert_called_once_with( + ["docker", "image", "build", "--no-cache", f"-t={tag}", "-"], + input=b"xyz", + ) + + out, _ = capsys.readouterr() + assert f"Successfully built {image} and tagged with {tag}" in out + assert "Image is not suitable for deploying/pushing" not in out + + +def test_build_image_no_tag(capsys, mock_docker_build): + m_stream, m_run = mock_docker_build + image = "hello-world" + + assert docker.build_image(image, None) is None + m_stream.assert_called_once() + m_run.assert_called_once_with( + ["docker", "image", "build", "--no-cache", "-"], + input=b"xyz", + ) + + out, _ = capsys.readouterr() + assert f"Successfully built {image}" in out + assert "Image is not suitable for deploying/pushing" in out