diff --git a/newsfragments/127-fix-git-build-url-context.bugfix b/newsfragments/127-fix-git-build-url-context.bugfix new file mode 100644 index 0000000..cc277cd --- /dev/null +++ b/newsfragments/127-fix-git-build-url-context.bugfix @@ -0,0 +1 @@ +- Fix using git URL as build context diff --git a/podman_compose.py b/podman_compose.py index 018e513..b206d7f 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -26,6 +26,7 @@ import signal import subprocess import sys import tempfile +import urllib.parse from asyncio import Task from enum import Enum @@ -1663,7 +1664,9 @@ def normalize_service_final(service: dict, project_dir: str) -> dict: if "build" in service: build = service["build"] context = build if isinstance(build, str) else build.get("context", ".") - context = os.path.normpath(os.path.join(project_dir, context)) + + if not is_path_git_url(context): + context = os.path.normpath(os.path.join(project_dir, context)) if not isinstance(service["build"], dict): service["build"] = {} service["build"]["context"] = context @@ -2501,12 +2504,17 @@ async def compose_push(compose, args): await compose.podman.run([], "push", [cnt["image"]]) +def is_path_git_url(path): + r = urllib.parse.urlparse(path) + return r.scheme == 'git' or r.path.endswith('.git') + + def container_to_build_args(compose, cnt, args, path_exists, cleanup_callbacks=None): build_desc = cnt["build"] if not hasattr(build_desc, "items"): build_desc = {"context": build_desc} ctx = build_desc.get("context", ".") - dockerfile = build_desc.get("dockerfile") + dockerfile = build_desc.get("dockerfile", "") dockerfile_inline = build_desc.get("dockerfile_inline") if dockerfile_inline is not None: dockerfile_inline = str(dockerfile_inline) @@ -2524,7 +2532,10 @@ def container_to_build_args(compose, cnt, args, path_exists, cleanup_callbacks=N if cleanup_callbacks is not None: list.append(cleanup_callbacks, cleanup_temp_dockfile) - else: + + build_args = [] + + if not is_path_git_url(ctx): if dockerfile: dockerfile = os.path.join(ctx, dockerfile) else: @@ -2540,9 +2551,16 @@ def container_to_build_args(compose, cnt, args, path_exists, cleanup_callbacks=N dockerfile = os.path.join(ctx, dockerfile) if path_exists(dockerfile): break - if not path_exists(dockerfile): - raise OSError("Dockerfile not found in " + ctx) - build_args = ["-f", dockerfile, "-t", cnt["image"]] + + if path_exists(dockerfile): + # normalize dockerfile path, as the user could have provided unpredictable file formats + dockerfile = os.path.normpath(os.path.join(ctx, dockerfile)) + build_args.extend(["-f", dockerfile]) + else: + raise OSError(f"Dockerfile not found in {ctx}") + + build_args.extend(["-t", cnt["image"]]) + if "platform" in cnt: build_args.extend(["--platform", cnt["platform"]]) for secret in build_desc.get("secrets", []): diff --git a/tests/integration/build/git_url_context/__init__.py b/tests/integration/build/git_url_context/__init__.py new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/tests/integration/build/git_url_context/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/integration/build/git_url_context/docker-compose.yml b/tests/integration/build/git_url_context/docker-compose.yml new file mode 100644 index 0000000..f57fd55 --- /dev/null +++ b/tests/integration/build/git_url_context/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3" +services: + test_context: + build: + context: https://github.com/mokibit/test-git-url-as-context.git + image: test-git-url-as-context + test_context_inline: + build: https://github.com/mokibit/test-git-url-as-context.git + image: test-git-url-as-context-inline diff --git a/tests/integration/build/git_url_context/test_podman_compose_build_git_url_context.py b/tests/integration/build/git_url_context/test_podman_compose_build_git_url_context.py new file mode 100644 index 0000000..99254d4 --- /dev/null +++ b/tests/integration/build/git_url_context/test_podman_compose_build_git_url_context.py @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: GPL-2.0 + +import os +from parameterized import parameterized +import unittest + +from tests.integration.test_utils import podman_compose_path +from tests.integration.test_utils import test_path +from tests.integration.test_utils import RunSubprocessMixin + + +def compose_yaml_path(): + """ "Returns the path to the compose file used for this test module""" + base_path = os.path.join(test_path(), "build/git_url_context") + return os.path.join(base_path, "docker-compose.yml") + + +class TestComposeBuildGitUrlAsContext(unittest.TestCase, RunSubprocessMixin): + @parameterized.expand([ + ("git_url_context_test_context_1", "data_1.txt", b'test1\r\n'), + ("git_url_context_test_context_1", "data_2.txt", b'test2\r\n'), + ("git_url_context_test_context_inline_1", "data_1.txt", b'test1\r\n'), + ("git_url_context_test_context_inline_1", "data_2.txt", b'test2\r\n'), + ]) + def test_build_git_url_as_context(self, container_name, file_name, output): + # test if container can access specific files from git repository when git url is used as + # a build context + try: + out, _ = self.run_subprocess_assert_returncode([ + podman_compose_path(), + "-f", + compose_yaml_path(), + "up", + "-d", + ]) + + out, _ = self.run_subprocess_assert_returncode([ + "podman", + "exec", + "-ti", + f"{container_name}", + "sh", + "-c", + f"cat {file_name}", + ]) + self.assertEqual(out, output) + finally: + out, _ = self.run_subprocess_assert_returncode([ + podman_compose_path(), + "-f", + compose_yaml_path(), + "down", + "-t", + "0", + ]) diff --git a/tests/unit/test_container_to_build_args.py b/tests/unit/test_container_to_build_args.py index f06a34d..cd52aca 100644 --- a/tests/unit/test_container_to_build_args.py +++ b/tests/unit/test_container_to_build_args.py @@ -46,7 +46,7 @@ class TestContainerToBuildArgs(unittest.TestCase): args, [ '-f', - './Containerfile', + 'Containerfile', '-t', 'new-image', '--no-cache', @@ -67,7 +67,7 @@ class TestContainerToBuildArgs(unittest.TestCase): args, [ '-f', - './Containerfile', + 'Containerfile', '-t', 'new-image', '--platform', @@ -90,7 +90,7 @@ class TestContainerToBuildArgs(unittest.TestCase): args, [ '-f', - './Containerfile', + 'Containerfile', '-t', 'new-image', '-t', @@ -115,7 +115,7 @@ class TestContainerToBuildArgs(unittest.TestCase): args, [ '-f', - './Containerfile', + 'Containerfile', '-t', 'new-image', '--label', @@ -141,7 +141,7 @@ class TestContainerToBuildArgs(unittest.TestCase): args, [ '-f', - './Containerfile', + 'Containerfile', '-t', 'new-image', '--no-cache', @@ -180,3 +180,42 @@ class TestContainerToBuildArgs(unittest.TestCase): for c in cleanup_callbacks: c() self.assertFalse(os.path.exists(temp_dockerfile)) + + def test_context_git_url(self): + c = create_compose_mock() + + cnt = get_minimal_container() + cnt['build']['context'] = "https://github.com/test_repo.git" + args = get_minimal_args() + + args = container_to_build_args(c, cnt, args, lambda path: False) + self.assertEqual( + args, + [ + '-t', + 'new-image', + '--no-cache', + '--pull-always', + 'https://github.com/test_repo.git', + ], + ) + + def test_context_invalid_git_url_git_is_not_prefix(self): + c = create_compose_mock() + + cnt = get_minimal_container() + cnt['build']['context'] = "not_prefix://github.com/test_repo" + args = get_minimal_args() + + with self.assertRaises(OSError): + container_to_build_args(c, cnt, args, lambda path: False) + + def test_context_invalid_git_url_git_is_not_suffix(self): + c = create_compose_mock() + + cnt = get_minimal_container() + cnt['build']['context'] = "https://github.com/test_repo.git/not_suffix" + args = get_minimal_args() + + with self.assertRaises(OSError): + container_to_build_args(c, cnt, args, lambda path: False) diff --git a/tests/unit/test_is_path_git_url.py b/tests/unit/test_is_path_git_url.py new file mode 100644 index 0000000..831a7a7 --- /dev/null +++ b/tests/unit/test_is_path_git_url.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0 + +import unittest + +from parameterized import parameterized + +from podman_compose import is_path_git_url + + +class TestIsPathGitUrl(unittest.TestCase): + @parameterized.expand([ + ("prefix_git", "git://host.xz/path/to/repo", True), + ("prefix_almost_git", "gitt://host.xz/path/to/repo", False), + ("prefix_wrong", "http://host.xz/path/to/repo", False), + ("suffix_git", "http://host.xz/path/to/repo.git", True), + ("suffix_wrong", "http://host.xz/path/to/repo", False), + ("suffix_with_url_fragment", "http://host.xz/path/to/repo.git#fragment", True), + ("suffix_and_prefix", "git://host.xz/path/to/repo.git", True), + ("empty_url_path", "http://#fragment", False), + ]) + def test_is_path_git_url(self, test_name, path, result): + self.assertEqual(is_path_git_url(path), result)