Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions pre_commit/languages/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down
55 changes: 55 additions & 0 deletions tests/languages/docker_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Loading