From a87861ef57434a4361abe7db3e1b52f4cb1690b8 Mon Sep 17 00:00:00 2001 From: Federico Rizzo Date: Fri, 20 Jun 2025 16:47:08 +0200 Subject: [PATCH 1/5] fix image build not using cache This commit fixes issue #528 by adding a default value to parameters layers and outputformat. This change aligns the behavior with podman-remote. Signed-off-by: Federico Rizzo --- podman/domain/images_build.py | 12 ++++++++++-- podman/tests/integration/test_images.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/podman/domain/images_build.py b/podman/domain/images_build.py index 66eb90f2..4802d186 100644 --- a/podman/domain/images_build.py +++ b/podman/domain/images_build.py @@ -204,8 +204,16 @@ def _render_params(kwargs) -> dict[str, list[Any]]: if "labels" in kwargs: params["labels"] = json.dumps(kwargs.get("labels")) - if params["dockerfile"] is None: - params["dockerfile"] = f".containerfile.{random.getrandbits(160):x}" + def default(value, def_value): + return def_value if value is None else value + + params["outputformat"] = default( + params["outputformat"], "application/vnd.oci.image.manifest.v1+json" + ) + params["layers"] = default(params["layers"], True) + params["dockerfile"] = default( + params["dockerfile"], f".containerfile.{random.getrandbits(160):x}" + ) # Remove any unset parameters return dict(filter(lambda i: i[1] is not None, params.items())) diff --git a/podman/tests/integration/test_images.py b/podman/tests/integration/test_images.py index a8636411..4f5b88dc 100644 --- a/podman/tests/integration/test_images.py +++ b/podman/tests/integration/test_images.py @@ -15,6 +15,7 @@ """Images integration tests.""" import io +import json import platform import tarfile import types @@ -144,6 +145,19 @@ def test_build(self): self.assertIsNotNone(image) self.assertIsNotNone(image.id) + def test_build_cache(self): + """Check that building twice the same image uses caching""" + buffer = io.StringIO("""FROM quay.io/libpod/alpine_labels:latest\nLABEL test=value""") + image, _ = self.client.images.build(fileobj=buffer) + buffer.seek(0) + _, stream = self.client.images.build(fileobj=buffer) + for line in stream: + # Search for a line with contents "-> Using cache " + parsed = json.loads(line)['stream'] + if "Using cache" in parsed: + break + self.assertEqual(parsed.split()[3], image.id) + def test_build_with_context(self): context = io.BytesIO() with tarfile.open(fileobj=context, mode="w") as tar: From 3cb64f56b9655917d8d8b93a4c3638e2ec743234 Mon Sep 17 00:00:00 2001 From: Federico Rizzo Date: Mon, 23 Jun 2025 18:27:26 +0200 Subject: [PATCH 2/5] document new defaults in images_build.py Signed-off-by: Federico Rizzo --- podman/domain/images_build.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/podman/domain/images_build.py b/podman/domain/images_build.py index 4802d186..857c267c 100644 --- a/podman/domain/images_build.py +++ b/podman/domain/images_build.py @@ -63,9 +63,10 @@ def build(self, **kwargs) -> tuple[Image, Iterator[bytes]]: isolation (str) – Isolation technology used during build. (ignored) use_config_proxy (bool) – (ignored) http_proxy (bool) - Inject http proxy environment variables into container (Podman only) - layers (bool) - Cache intermediate layers during build. + layers (bool) - Cache intermediate layers during build. Default True. output (str) - specifies if any custom build output is selected for following build. outputformat (str) - The format of the output image's manifest and configuration data. + Default to "application/vnd.oci.image.manifest.v1+json" (OCI format). Returns: first item is the podman.domain.images.Image built From 512c8617faf96016c62e5e582b064f2e6bf09674 Mon Sep 17 00:00:00 2001 From: Federico Rizzo Date: Mon, 23 Jun 2025 18:28:24 +0200 Subject: [PATCH 3/5] use kwargs.get default parameter for defaults Replace function "default" inside _render_params in images_build.py with dict.get (kwargs.get) default value. Signed-off-by: Federico Rizzo --- podman/domain/images_build.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/podman/domain/images_build.py b/podman/domain/images_build.py index 857c267c..00f35678 100644 --- a/podman/domain/images_build.py +++ b/podman/domain/images_build.py @@ -168,7 +168,7 @@ def _render_params(kwargs) -> dict[str, list[Any]]: raise PodmanError("Custom encoding not supported when gzip enabled.") params = { - "dockerfile": kwargs.get("dockerfile"), + "dockerfile": kwargs.get("dockerfile", f".containerfile.{random.getrandbits(160):x}"), "forcerm": kwargs.get("forcerm"), "httpproxy": kwargs.get("http_proxy"), "networkmode": kwargs.get("network_mode"), @@ -182,9 +182,11 @@ def _render_params(kwargs) -> dict[str, list[Any]]: "squash": kwargs.get("squash"), "t": kwargs.get("tag"), "target": kwargs.get("target"), - "layers": kwargs.get("layers"), + "layers": kwargs.get("layers", True), "output": kwargs.get("output"), - "outputformat": kwargs.get("outputformat"), + "outputformat": kwargs.get( + "outputformat", "application/vnd.oci.image.manifest.v1+json" + ), } if "buildargs" in kwargs: @@ -205,16 +207,5 @@ def _render_params(kwargs) -> dict[str, list[Any]]: if "labels" in kwargs: params["labels"] = json.dumps(kwargs.get("labels")) - def default(value, def_value): - return def_value if value is None else value - - params["outputformat"] = default( - params["outputformat"], "application/vnd.oci.image.manifest.v1+json" - ) - params["layers"] = default(params["layers"], True) - params["dockerfile"] = default( - params["dockerfile"], f".containerfile.{random.getrandbits(160):x}" - ) - # Remove any unset parameters return dict(filter(lambda i: i[1] is not None, params.items())) From 9e37dc0d025b0df71332fcd3e4e80e5ee554dc83 Mon Sep 17 00:00:00 2001 From: Federico Rizzo Date: Tue, 24 Jun 2025 15:59:32 +0200 Subject: [PATCH 4/5] fix wrong mock URL in test_build unit test Signed-off-by: Federico Rizzo --- podman/tests/unit/test_build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/podman/tests/unit/test_build.py b/podman/tests/unit/test_build.py index 6adf0fe8..721ff815 100644 --- a/podman/tests/unit/test_build.py +++ b/podman/tests/unit/test_build.py @@ -130,13 +130,13 @@ def test_build_logged_error(self, mock_prepare_containerfile, mock_create_tar): @requests_mock.Mocker() def test_build_no_context(self, mock): - mock.post(tests.LIBPOD_URL + "/images/build") + mock.post(tests.LIBPOD_URL + "/build") with self.assertRaises(TypeError): self.client.images.build() @requests_mock.Mocker() def test_build_encoding(self, mock): - mock.post(tests.LIBPOD_URL + "/images/build") + mock.post(tests.LIBPOD_URL + "/build") with self.assertRaises(DockerException): self.client.images.build(path="/root", gzip=True, encoding="utf-8") From 63f91a77ed166f4d9f8a4a71cd1aa5cab7f8b599 Mon Sep 17 00:00:00 2001 From: Federico Rizzo Date: Tue, 24 Jun 2025 16:01:00 +0200 Subject: [PATCH 5/5] add unit test for images.build default parameters Signed-off-by: Federico Rizzo --- podman/tests/unit/test_build.py | 66 +++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/podman/tests/unit/test_build.py b/podman/tests/unit/test_build.py index 721ff815..207d2864 100644 --- a/podman/tests/unit/test_build.py +++ b/podman/tests/unit/test_build.py @@ -1,4 +1,5 @@ import io +import requests import json import unittest @@ -16,6 +17,20 @@ from podman.domain.images import Image from podman.errors import BuildError, DockerException +good_image_id = "032b8b2855fc" +good_stream = [ + {"stream": " ---\u003e a9eb17255234"}, + {"stream": "Step 1 : VOLUME /data"}, + {"stream": " ---\u003e Running in abdc1e6896c6"}, + {"stream": " ---\u003e 713bca62012e"}, + {"stream": "Removing intermediate container abdc1e6896c6"}, + {"stream": "Step 2 : CMD [\"/bin/sh\"]"}, + {"stream": " ---\u003e Running in dba30f2a1a7e"}, + {"stream": " ---\u003e 032b8b2855fc"}, + {"stream": "Removing intermediate container dba30f2a1a7e"}, + {"stream": f"{good_image_id}\n"}, +] + class TestBuildCase(unittest.TestCase): """Test ImagesManager build(). @@ -41,19 +56,7 @@ def test_build(self, mock_prepare_containerfile, mock_create_tar): mock_prepare_containerfile.return_value = "Containerfile" mock_create_tar.return_value = b"This is a mocked tarball." - stream = [ - {"stream": " ---\u003e a9eb17255234"}, - {"stream": "Step 1 : VOLUME /data"}, - {"stream": " ---\u003e Running in abdc1e6896c6"}, - {"stream": " ---\u003e 713bca62012e"}, - {"stream": "Removing intermediate container abdc1e6896c6"}, - {"stream": "Step 2 : CMD [\"/bin/sh\"]"}, - {"stream": " ---\u003e Running in dba30f2a1a7e"}, - {"stream": " ---\u003e 032b8b2855fc"}, - {"stream": "Removing intermediate container dba30f2a1a7e"}, - {"stream": "032b8b2855fc\n"}, - ] - + stream = good_stream buffer = io.StringIO() for entry in stream: buffer.write(json.JSONEncoder().encode(entry)) @@ -70,9 +73,9 @@ def test_build(self, mock_prepare_containerfile, mock_create_tar): text=buffer.getvalue(), ) mock.get( - tests.LIBPOD_URL + "/images/032b8b2855fc/json", + tests.LIBPOD_URL + f"/images/{good_image_id}/json", json={ - "Id": "032b8b2855fc", + "Id": good_image_id, "ParentId": "", "RepoTags": ["fedora:latest", "fedora:33", ":"], "RepoDigests": [ @@ -100,7 +103,7 @@ def test_build(self, mock_prepare_containerfile, mock_create_tar): labels={"Unittest": "true"}, ) self.assertIsInstance(image, Image) - self.assertEqual(image.id, "032b8b2855fc") + self.assertEqual(image.id, good_image_id) self.assertIsInstance(logs, Iterable) @patch.object(api, "create_tar") @@ -140,6 +143,37 @@ def test_build_encoding(self, mock): with self.assertRaises(DockerException): self.client.images.build(path="/root", gzip=True, encoding="utf-8") + @patch.object(api, "create_tar") + @patch.object(api, "prepare_containerfile") + def test_build_defaults(self, mock_prepare_containerfile, mock_create_tar): + """Check the defaults used by images.build""" + mock_prepare_containerfile.return_value = "Containerfile" + mock_create_tar.return_value = b"This is a mocked tarball." + + stream = good_stream + buffer = io.StringIO() + for entry in stream: + buffer.write(json.dumps(entry)) + buffer.write("\n") + + with requests_mock.Mocker() as mock: + query = "?outputformat=" + ( + requests.utils.quote("application/vnd.oci.image.manifest.v1+json", safe='') + + "&layers=True" + ) + mock.post( + tests.LIBPOD_URL + "/build" + query, + text=buffer.getvalue(), + ) + mock.get( + tests.LIBPOD_URL + f"/images/{good_image_id}/json", + json={ + "Id": "unittest", + }, + ) + img, _ = self.client.images.build(path="/tmp/context_dir") + assert img.id == "unittest" + if __name__ == '__main__': unittest.main()