mirror of
https://github.com/pre-commit/pre-commit.git
synced 2026-02-18 00:24:47 +04:00
By running containers in a rootless docker context as root. This is because user and group IDs are remapped in the user namespaces uses by rootless docker, and it's unlikely that the current user ID will map to the same ID under this remap (see docs[1] for some more details). Specifically, it means ownership of mounted volumes will not be for the current user and trying to write can result in permission errors. This change borrows heavily from an existing PR[2]. The output format of `docker system info` I don't think is documented/guaranteed anywhere, but it should corresponding to the format of a `/info` API request to Docker[3] The added test _hopes_ to avoid regressions in this behaviour, but since tests aren't run in a rootless docker context on the PR checks (and I couldn't find an easy way to make it the case) there's still a risk of regressions sneaking in. Link: https://docs.docker.com/engine/security/rootless/ [1] Link: https://github.com/pre-commit/pre-commit/pull/1484/ [2] Link: https://docs.docker.com/reference/api/engine/version/v1.48/#tag/System/operation/SystemAuth [3] Co-authored-by: Kurt von Laven <Kurt-von-Laven@users.noreply.github.com> Co-authored-by: Fabrice Flore-Thébault <ffloreth@redhat.com>
177 lines
5.5 KiB
Python
177 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import functools
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from collections.abc import Sequence
|
|
|
|
from pre_commit import lang_base
|
|
from pre_commit.prefix import Prefix
|
|
from pre_commit.util import CalledProcessError
|
|
from pre_commit.util import cmd_output_b
|
|
|
|
ENVIRONMENT_DIR = 'docker'
|
|
PRE_COMMIT_LABEL = 'PRE_COMMIT'
|
|
get_default_version = lang_base.basic_get_default_version
|
|
health_check = lang_base.basic_health_check
|
|
in_env = lang_base.no_env # no special environment for docker
|
|
|
|
|
|
def _is_in_docker() -> bool:
|
|
try:
|
|
with open('/proc/1/cgroup', 'rb') as f:
|
|
return b'docker' in f.read()
|
|
except FileNotFoundError:
|
|
return False
|
|
|
|
|
|
def _get_container_id() -> str:
|
|
# It's assumed that we already check /proc/1/cgroup in _is_in_docker. The
|
|
# cpuset cgroup controller existed since cgroups were introduced so this
|
|
# way of getting the container ID is pretty reliable.
|
|
with open('/proc/1/cgroup', 'rb') as f:
|
|
for line in f.readlines():
|
|
if line.split(b':')[1] == b'cpuset':
|
|
return os.path.basename(line.split(b':')[2]).strip().decode()
|
|
raise RuntimeError('Failed to find the container ID in /proc/1/cgroup.')
|
|
|
|
|
|
def _get_docker_path(path: str) -> str:
|
|
if not _is_in_docker():
|
|
return path
|
|
|
|
container_id = _get_container_id()
|
|
|
|
try:
|
|
_, out, _ = cmd_output_b('docker', 'inspect', container_id)
|
|
except CalledProcessError:
|
|
# self-container was not visible from here (perhaps docker-in-docker)
|
|
return path
|
|
|
|
container, = json.loads(out)
|
|
for mount in container['Mounts']:
|
|
src_path = mount['Source']
|
|
to_path = mount['Destination']
|
|
if os.path.commonpath((path, to_path)) == to_path:
|
|
# So there is something in common,
|
|
# and we can proceed remapping it
|
|
return path.replace(to_path, src_path)
|
|
# we're in Docker, but the path is not mounted, cannot really do anything,
|
|
# so fall back to original path
|
|
return path
|
|
|
|
|
|
def md5(s: str) -> str: # pragma: win32 no cover
|
|
return hashlib.md5(s.encode()).hexdigest()
|
|
|
|
|
|
def docker_tag(prefix: Prefix) -> str: # pragma: win32 no cover
|
|
md5sum = md5(os.path.basename(prefix.prefix_dir)).lower()
|
|
return f'pre-commit-{md5sum}'
|
|
|
|
|
|
def build_docker_image(
|
|
prefix: Prefix,
|
|
*,
|
|
pull: bool,
|
|
) -> None: # pragma: win32 no cover
|
|
cmd: tuple[str, ...] = (
|
|
'docker', 'build',
|
|
'--tag', docker_tag(prefix),
|
|
'--label', PRE_COMMIT_LABEL,
|
|
)
|
|
if pull:
|
|
cmd += ('--pull',)
|
|
# This must come last for old versions of docker. See #477
|
|
cmd += ('.',)
|
|
lang_base.setup_cmd(prefix, cmd)
|
|
|
|
|
|
def install_environment(
|
|
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
|
|
) -> None: # pragma: win32 no cover
|
|
lang_base.assert_version_default('docker', version)
|
|
lang_base.assert_no_additional_deps('docker', additional_dependencies)
|
|
|
|
directory = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
|
|
|
# Docker doesn't really have relevant disk environment, but pre-commit
|
|
# still needs to cleanup its state files on failure
|
|
build_docker_image(prefix, pull=True)
|
|
os.mkdir(directory)
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
def _is_rootless() -> bool: # pragma: win32 no cover
|
|
retcode, out, _ = cmd_output_b(
|
|
'docker', 'system', 'info', '--format', '{{ json . }}',
|
|
)
|
|
if retcode != 0:
|
|
return False
|
|
|
|
info = json.loads(out)
|
|
try:
|
|
return (
|
|
# docker:
|
|
# https://docs.docker.com/reference/api/engine/version/v1.48/#tag/System/operation/SystemInfo
|
|
'name=rootless' in info.get('SecurityOptions', ()) or
|
|
# podman:
|
|
# https://docs.podman.io/en/latest/_static/api.html?version=v5.4#tag/system/operation/SystemInfoLibpod
|
|
info['host']['security']['rootless']
|
|
)
|
|
except KeyError:
|
|
return False
|
|
|
|
|
|
def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover
|
|
if _is_rootless():
|
|
return ()
|
|
|
|
try:
|
|
return ('-u', f'{os.getuid()}:{os.getgid()}')
|
|
except AttributeError:
|
|
return ()
|
|
|
|
|
|
def get_docker_tty(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover # noqa: E501
|
|
return (('--tty',) if color else ())
|
|
|
|
|
|
def docker_cmd(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover
|
|
return (
|
|
'docker', 'run',
|
|
'--rm',
|
|
*get_docker_tty(color=color),
|
|
*get_docker_user(),
|
|
# https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from
|
|
# The `Z` option tells Docker to label the content with a private
|
|
# unshared label. Only the current container can use a private volume.
|
|
'-v', f'{_get_docker_path(os.getcwd())}:/src:rw,Z',
|
|
'--workdir', '/src',
|
|
)
|
|
|
|
|
|
def run_hook(
|
|
prefix: Prefix,
|
|
entry: str,
|
|
args: Sequence[str],
|
|
file_args: Sequence[str],
|
|
*,
|
|
is_local: bool,
|
|
require_serial: bool,
|
|
color: bool,
|
|
) -> tuple[int, bytes]: # pragma: win32 no cover
|
|
# Rebuild the docker image in case it has gone missing, as many people do
|
|
# automated cleanup of docker images.
|
|
build_docker_image(prefix, pull=False)
|
|
|
|
entry_exe, *cmd_rest = lang_base.hook_cmd(entry, args)
|
|
|
|
entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix))
|
|
return lang_base.run_xargs(
|
|
(*docker_cmd(color=color), *entry_tag, *cmd_rest),
|
|
file_args,
|
|
require_serial=require_serial,
|
|
color=color,
|
|
)
|