From 998b646db9a844dbcc53431efcb45f7df0a90c78 Mon Sep 17 00:00:00 2001 From: Rob Stewart Date: Fri, 16 Oct 2026 11:50:24 +0100 Subject: [PATCH] add PRE_COMMIT_CONTAINER_RUNTIME to select docker or podman An opt-in, validated environment variable that selects the executable used by the docker and docker_image languages. Unset or empty keeps the current behaviour (docker). Values are trimmed and lowercased and must be docker or podman, anything else is a FatalError. --- pre_commit/languages/docker.py | 25 +++++++++++++--- tests/languages/docker_test.py | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 7f45ac865..023260ad3 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -9,6 +9,7 @@ from collections.abc import Sequence from pre_commit import lang_base +from pre_commit.errors import FatalError from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output_b @@ -19,6 +20,8 @@ health_check = lang_base.basic_health_check in_env = lang_base.no_env # no special environment for docker +CONTAINER_RUNTIMES = ('docker', 'podman') + _HOSTNAME_MOUNT_RE = re.compile( rb""" /containers @@ -31,6 +34,20 @@ ) +def _docker_exe() -> str: + runtime = os.environ.get('PRE_COMMIT_CONTAINER_RUNTIME', '') + runtime = runtime.strip().lower() + if not runtime: + return 'docker' + elif runtime in CONTAINER_RUNTIMES: + return runtime + else: + raise FatalError( + f'PRE_COMMIT_CONTAINER_RUNTIME={runtime!r} is not supported, ' + f'expected one of: {", ".join(CONTAINER_RUNTIMES)}', + ) + + def _get_container_id() -> str | None: with contextlib.suppress(FileNotFoundError): with open('/proc/1/mountinfo', 'rb') as f: @@ -48,7 +65,7 @@ def _get_docker_path(path: str) -> str: return path try: - _, out, _ = cmd_output_b('docker', 'inspect', container_id) + _, out, _ = cmd_output_b(_docker_exe(), 'inspect', container_id) except CalledProcessError: # self-container was not visible from here (perhaps docker-in-docker) return path @@ -81,7 +98,7 @@ def build_docker_image( pull: bool, ) -> None: # pragma: win32 no cover cmd: tuple[str, ...] = ( - 'docker', 'build', + _docker_exe(), 'build', '--tag', docker_tag(prefix), '--label', PRE_COMMIT_LABEL, ) @@ -109,7 +126,7 @@ def install_environment( @functools.lru_cache(maxsize=1) def _is_rootless() -> bool: # pragma: win32 no cover retcode, out, _ = cmd_output_b( - 'docker', 'system', 'info', '--format', '{{ json . }}', + _docker_exe(), 'system', 'info', '--format', '{{ json . }}', ) if retcode != 0: return False @@ -144,7 +161,7 @@ def get_docker_tty(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover def docker_cmd(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover return ( - 'docker', 'run', + _docker_exe(), 'run', '--rm', *get_docker_tty(color=color), *get_docker_user(), diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index e269976f7..c829db46d 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -9,6 +9,8 @@ import pytest +from pre_commit import envcontext +from pre_commit.errors import FatalError from pre_commit.languages import docker from pre_commit.util import CalledProcessError from testing.language_helpers import run_language @@ -195,6 +197,59 @@ def invalid_attribute(): assert docker.get_docker_user() == () +@pytest.mark.parametrize( + ('value', 'expected'), + ( + pytest.param(envcontext.UNSET, 'docker', id='unset'), + pytest.param('', 'docker', id='empty'), + pytest.param(' ', 'docker', id='whitespace-only'), + pytest.param('docker', 'docker', id='docker'), + pytest.param('podman', 'podman', id='podman'), + pytest.param('PODMAN', 'podman', id='uppercase'), + pytest.param(' Podman ', 'podman', id='trimmed-and-lowercased'), + ), +) +def test_docker_exe(value, expected): + ctx = (('PRE_COMMIT_CONTAINER_RUNTIME', value),) + with envcontext.envcontext(ctx): + assert docker._docker_exe() == expected + + +@pytest.mark.parametrize( + 'value', + ('nerdctl', '/usr/bin/podman', 'podman --remote', 'docker;podman'), +) +def test_docker_exe_unsupported_runtime(value): + ctx = (('PRE_COMMIT_CONTAINER_RUNTIME', value),) + with envcontext.envcontext(ctx): + with pytest.raises(FatalError) as excinfo: + docker._docker_exe() + msg, = excinfo.value.args + assert 'PRE_COMMIT_CONTAINER_RUNTIME' in msg + assert 'docker, podman' in msg + + +@pytest.mark.parametrize('runtime', ('docker', 'podman')) +def test_docker_cmd_uses_runtime(runtime): + ctx = (('PRE_COMMIT_CONTAINER_RUNTIME', runtime),) + with envcontext.envcontext(ctx), mock.patch.object( + docker, 'get_docker_user', return_value=(), + ): + assert docker.docker_cmd(color=False)[:2] == (runtime, 'run') + + +@pytest.mark.parametrize('runtime', ('docker', 'podman')) +def test_docker_rootless_check_uses_runtime(runtime): + ctx = (('PRE_COMMIT_CONTAINER_RUNTIME', runtime),) + ret = (0, b'{}', b'') + with envcontext.envcontext(ctx): + with mock.patch.object( + docker, 'cmd_output_b', return_value=ret, + ) as mck: + docker._is_rootless() + assert mck.call_args[0][0] == runtime + + @pytest.fixture(autouse=True) def _avoid_cache(): with mock.patch.object(